-
module Api
-
module Admin
-
class AdminController < ApplicationController
-
include AdminAuthorizable
-
before_action :authenticate_admin!
-
-
# GET /api/admin/dashboard
-
def dashboard
-
render json: {
-
current_user: {
-
id: current_user.id,
-
nickname: current_user.nickname,
-
role: current_user.role_display_name,
-
permissions: current_user_permissions
-
},
-
system_stats: {
-
total_users: User.count,
-
total_posts: Post.count,
-
visible_posts: Post.visible.count,
-
total_events: ReadingEvent.count,
-
pending_events: ReadingEvent.where(approval_status: :pending).count,
-
active_events: ReadingEvent.where(status: :in_progress).count,
-
admin_count: User.where(role: :admin).count,
-
root_count: User.where(role: :root).count
-
},
-
available_actions: admin_available_actions
-
}
-
end
-
-
# GET /api/admin/users
-
def users
-
authenticate_root! # 只有root可以查看所有用户
-
-
users = User.select(:id, :nickname, :role, :created_at, :wx_openid)
-
.order(created_at: :desc)
-
-
render json: {
-
users: users.map { |user|
-
{
-
id: user.id,
-
nickname: user.nickname,
-
role: user.role_display_name,
-
role_value: user.role,
-
created_at: user.created_at,
-
permissions: user_permissions_for(user)
-
}
-
},
-
summary: {
-
total: users.count,
-
by_role: {
-
user: users.select(&:user?).count,
-
admin: users.select(&:admin?).count,
-
root: users.select(&:root?).count
-
}
-
}
-
}
-
end
-
-
# PUT /api/admin/users/:id/promote_admin
-
def promote_user_to_admin
-
authenticate_root! # 只有root可以提升管理员
-
-
user = User.find(params[:id])
-
if user.root?
-
return render json: { error: "不能提升超级管理员" }, status: :unprocessable_entity
-
end
-
-
if user.update!(role: :admin)
-
render json: {
-
message: "用户已提升为管理员",
-
user: {
-
id: user.id,
-
nickname: user.nickname,
-
new_role: user.role_display_name
-
}
-
}
-
else
-
render json: { error: "提升失败" }, status: :unprocessable_entity
-
end
-
end
-
-
# PUT /api/admin/users/:id/demote
-
def demote_user
-
authenticate_root! # 只有root可以降级用户
-
-
user = User.find(params[:id])
-
if user.root?
-
return render json: { error: "不能降级超级管理员" }, status: :unprocessable_entity
-
end
-
-
if user.update!(role: :participant)
-
render json: {
-
message: "用户已降级为参与者",
-
user: {
-
id: user.id,
-
nickname: user.nickname,
-
new_role: user.role_display_name
-
}
-
}
-
else
-
render json: { error: "降级失败" }, status: :unprocessable_entity
-
end
-
end
-
-
# GET /api/admin/events/pending
-
def pending_events
-
events = ReadingEvent.includes(:leader)
-
.where(approval_status: :pending)
-
.order(created_at: :desc)
-
-
render json: {
-
events: events.map { |event|
-
{
-
id: event.id,
-
title: event.title,
-
book_name: event.book_name,
-
leader: {
-
id: event.leader.id,
-
nickname: event.leader.nickname
-
},
-
created_at: event.created_at,
-
enrollment_fee: event.enrollment_fee,
-
max_participants: event.max_participants
-
}
-
},
-
count: events.count
-
}
-
end
-
-
# POST /api/admin/init_root
-
def init_root_user
-
# 这个接口用于系统初始化时创建root用户
-
# 应该在系统部署后立即调用,然后禁用
-
-
if User.exists?(role: :root)
-
return render json: { error: "Root用户已存在" }, status: :unprocessable_entity
-
end
-
-
# 这里应该有更严格的验证,比如特定的token或者IP限制
-
# 为了演示,这里简化处理
-
root_info = params.require(:root).permit(:wx_openid, :nickname, :avatar_url)
-
-
user = User.new(root_info)
-
user.role = :root
-
-
if user.save
-
render json: {
-
message: "Root用户创建成功",
-
user: {
-
id: user.id,
-
nickname: user.nickname,
-
role: user.role_display_name
-
}
-
}
-
else
-
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def current_user_permissions
-
[
-
"approve_events",
-
"view_admin_panel"
-
].select { |perm| current_user.has_permission?(perm.to_sym) }
-
end
-
-
def admin_available_actions
-
actions = []
-
actions << { action: "approve_events", description: "审批活动" } if current_user.can_approve_events?
-
actions << { action: "manage_users", description: "管理用户" } if current_user.can_manage_users?
-
actions << { action: "view_admin_panel", description: "查看管理面板" } if current_user.can_view_admin_panel?
-
actions << { action: "manage_system", description: "管理系统" } if current_user.can_manage_system?
-
actions
-
end
-
-
def user_permissions_for(user)
-
permissions = []
-
permissions << "approve_events" if user.can_approve_events?
-
permissions << "manage_users" if user.can_manage_users?
-
permissions << "view_admin_panel" if user.can_view_admin_panel?
-
permissions << "manage_system" if user.can_manage_system?
-
permissions
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
module Api
-
class ApplicationController < ActionController::API
-
# 简单的健康检查
-
def health
-
render json: {
-
status: "ok",
-
timestamp: Time.current.iso8601,
-
environment: Rails.env,
-
version: "1.0.0"
-
}
-
end
-
-
private
-
-
# 从 JWT token 中获取当前用户
-
def current_user
-
return unless auth_header_present?
-
return unless auth_token_valid?
-
-
user_id = decoded_jwt_token['user_id']
-
@current_user ||= User.find_by(id: user_id)
-
end
-
-
# 检查是否需要用户认证
-
def authenticate_user!
-
return if current_user
-
-
# 提供更详细的错误信息用于调试
-
error_info = determine_auth_error
-
Rails.logger.warn "认证失败: #{error_info[:reason]} - #{error_info[:details]}"
-
-
render json: {
-
error: error_info[:message],
-
error_code: error_info[:code],
-
details: Rails.env.development? ? error_info[:details] : nil
-
}, status: :unauthorized
-
end
-
-
private
-
-
def auth_header_present?
-
request.headers['Authorization'].present?
-
end
-
-
def auth_token_valid?
-
auth_header = request.headers['Authorization']
-
token = auth_header.split(' ').last if auth_header
-
return false unless token
-
-
decoded_token = User.decode_jwt_token(token)
-
return false unless decoded_token
-
-
# 检查 token 是否过期
-
Time.current < Time.at(decoded_token['exp'])
-
end
-
-
def decoded_jwt_token
-
auth_header = request.headers['Authorization']
-
token = auth_header.split(' ').last if auth_header
-
@decoded_jwt_token ||= User.decode_jwt_token(token) if token
-
end
-
-
# 分析认证失败的具体原因
-
def determine_auth_error
-
auth_header = request.headers['Authorization']
-
-
# 1. 检查是否有Authorization头
-
unless auth_header.present?
-
return {
-
code: 'MISSING_AUTH_HEADER',
-
message: '缺少认证信息',
-
reason: 'no_auth_header',
-
details: '请求头中缺少Authorization字段'
-
}
-
end
-
-
# 2. 检查Authorization格式
-
unless auth_header.start_with?('Bearer ')
-
return {
-
code: 'INVALID_AUTH_FORMAT',
-
message: '认证格式错误',
-
reason: 'invalid_auth_format',
-
details: "Authorization头格式应为'Bearer <token>',当前为: #{auth_header[0..50]}..."
-
}
-
end
-
-
# 3. 提取token
-
token = auth_header.split(' ').last
-
unless token.present?
-
return {
-
code: 'MISSING_TOKEN',
-
message: '缺少认证令牌',
-
reason: 'missing_token',
-
details: 'Authorization头中缺少token部分'
-
}
-
end
-
-
# 4. 检查token格式
-
unless token.include?('.')
-
return {
-
code: 'INVALID_TOKEN_FORMAT',
-
message: '令牌格式错误',
-
reason: 'invalid_token_format',
-
details: 'JWT token应包含三个部分,用点分隔'
-
}
-
end
-
-
# 5. 尝试解码token
-
begin
-
decoded = User.decode_jwt_token(token)
-
unless decoded
-
return {
-
code: 'INVALID_TOKEN',
-
message: '令牌无效',
-
reason: 'decode_failed',
-
details: 'JWT token解码失败,可能被篡改或格式错误'
-
}
-
end
-
-
# 6. 检查token是否过期
-
exp_time = decoded['exp']
-
if exp_time
-
current_time = Time.current.to_i
-
if current_time >= exp_time
-
expired_time = Time.at(exp_time)
-
return {
-
code: 'TOKEN_EXPIRED',
-
message: '令牌已过期',
-
reason: 'token_expired',
-
details: "Token已于#{expired_time.strftime('%Y-%m-%d %H:%M:%S')}过期"
-
}
-
end
-
end
-
-
# 7. 检查用户是否存在
-
user_id = decoded['user_id']
-
unless user_id
-
return {
-
code: 'INVALID_TOKEN_PAYLOAD',
-
message: '令牌内容无效',
-
reason: 'missing_user_id',
-
details: 'Token中缺少user_id字段'
-
}
-
end
-
-
user = User.find_by(id: user_id)
-
unless user
-
return {
-
code: 'USER_NOT_FOUND',
-
message: '用户不存在',
-
reason: 'user_not_found',
-
details: "Token中用户ID(#{user_id})对应的用户不存在"
-
}
-
end
-
-
# 8. 检查用户状态(如果需要)
-
# 这里可以添加用户状态检查逻辑
-
-
rescue => e
-
return {
-
code: 'TOKEN_PROCESSING_ERROR',
-
message: '令牌处理错误',
-
reason: 'processing_error',
-
details: "处理token时发生错误: #{e.message}"
-
}
-
end
-
-
# 未知错误
-
{
-
code: 'UNKNOWN_AUTH_ERROR',
-
message: '认证失败',
-
reason: 'unknown',
-
details: '认证过程中发生未知错误'
-
}
-
end
-
end
-
end
-
module Api
-
class AuthController < Api::ApplicationController
-
before_action :authenticate_user!, only: [:me, :update_profile]
-
-
# 引入Service
-
def authentication_service
-
AuthenticationService
-
end
-
-
# 模拟登录(测试用)
-
def mock_login
-
# 使用AuthenticationService处理模拟登录
-
service_result = authentication_service.mock_login!(params.to_unsafe_h)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
render json: { error: service_result.first_error }, status: :unprocessable_entity
-
end
-
end
-
-
# 微信登录(新版,支持用户信息传递)
-
def wechat_login
-
# 使用AuthenticationService处理微信登录,支持传递用户信息
-
service_result = authentication_service.wechat_login!(params.to_unsafe_h)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
error_message = service_result.first_error
-
status_code = error_message.include?("code") ? :bad_request : :unauthorized
-
render json: { error: error_message }, status: status_code
-
end
-
end
-
-
# 微信登录(生产用)
-
def login
-
# 使用AuthenticationService处理微信登录
-
service_result = authentication_service.wechat_login!(params.to_unsafe_h)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
error_message = service_result.first_error
-
status_code = error_message.include?("code") ? :bad_request : :unauthorized
-
render json: { error: error_message }, status: status_code
-
end
-
end
-
-
# 获取当前用户信息
-
def me
-
render json: {
-
user: current_user.as_json_for_api
-
}
-
end
-
-
# 刷新访问令牌
-
def refresh_token
-
refresh_token_param = params[:refresh_token]
-
-
unless refresh_token_param.present?
-
return render json: {
-
error: '缺少refresh_token参数',
-
error_code: 'MISSING_REFRESH_TOKEN'
-
}, status: :bad_request
-
end
-
-
result = User.refresh_access_token(refresh_token_param)
-
-
if result
-
render json: {
-
message: 'Token刷新成功',
-
access_token: result[:access_token],
-
refresh_token: result[:refresh_token],
-
user: result[:user]
-
}
-
else
-
render json: {
-
error: 'refresh_token无效或已过期',
-
error_code: 'INVALID_REFRESH_TOKEN'
-
}, status: :unauthorized
-
end
-
end
-
-
# 更新用户资料
-
def update_profile
-
if current_user.update(profile_params)
-
render json: {
-
message: "更新成功",
-
user: {
-
id: current_user.id,
-
nickname: current_user.nickname,
-
avatar_url: current_user.avatar_url,
-
phone: current_user.phone
-
}
-
}
-
else
-
render json: { errors: current_user.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# fetch_wechat_openid方法已移至AuthenticationService
-
-
def profile_params
-
params.require(:user).permit(:nickname, :avatar_url, :phone)
-
end
-
end
-
end
-
module Api
-
class CheckInCommentsController < Api::ApplicationController
-
include Commentable
-
-
before_action :set_check_in, only: [:index, :create]
-
-
private
-
-
def fetch_comments
-
@check_in.comments
-
end
-
-
def build_comment(params)
-
comment = @check_in.comments.new(params)
-
# 打卡评论不需要post_id
-
comment.post_id = nil
-
comment
-
end
-
-
def format_single_comment(comment, can_edit = false)
-
# 为打卡评论使用专门的JSON格式,保持兼容性
-
{
-
id: comment.id,
-
content: comment.content,
-
created_at: comment.created_at,
-
updated_at: comment.updated_at,
-
author_info: {
-
id: comment.user.id,
-
nickname: comment.user.nickname,
-
avatar_url: comment.user.avatar_url
-
},
-
can_edit_current_user: can_edit || can_edit_comment?(comment, current_user)
-
}
-
end
-
-
def set_check_in
-
@check_in = CheckIn.find(params[:check_in_id])
-
rescue ActiveRecord::RecordNotFound
-
render_not_found('打卡不存在')
-
end
-
end
-
end
-
class Api::CheckInsController < ApplicationController
-
include Authenticable
-
-
# POST /api/reading_schedules/:reading_schedule_id/check_ins
-
def create
-
reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
-
enrollment = current_user.enrollments.find_by(reading_event: reading_schedule.reading_event)
-
-
unless enrollment
-
return render json: { error: "未报名该活动" }, status: :unprocessable_entity
-
end
-
-
check_in = CheckIn.new(check_in_params)
-
check_in.user = current_user
-
check_in.reading_schedule = reading_schedule
-
check_in.enrollment = enrollment
-
-
if check_in.save
-
render json: check_in, status: :created
-
else
-
render json: { error: check_in.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# GET /api/reading_schedules/:reading_schedule_id/check_ins
-
def index
-
reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
-
check_ins = reading_schedule.check_ins.includes(:user, :flower)
-
-
render json: check_ins.map { |ci|
-
{
-
id: ci.id,
-
user: {
-
id: ci.user.id,
-
nickname: ci.user.nickname,
-
avatar_url: ci.user.avatar_url
-
},
-
content: ci.content,
-
word_count: ci.word_count,
-
status: ci.status,
-
submitted_at: ci.submitted_at,
-
has_flower: ci.has_flower?,
-
flower: ci.flower ? {
-
giver: {
-
id: ci.flower.giver.id,
-
nickname: ci.flower.giver.nickname
-
},
-
comment: ci.flower.comment
-
} : nil
-
}
-
}
-
end
-
-
# GET /api/check_ins/:id
-
def show
-
check_in = CheckIn.includes(:user, :reading_schedule, :flower).find(params[:id])
-
-
render json: {
-
id: check_in.id,
-
user: {
-
id: check_in.user.id,
-
nickname: check_in.user.nickname,
-
avatar_url: check_in.user.avatar_url
-
},
-
reading_schedule: {
-
id: check_in.reading_schedule.id,
-
day_number: check_in.reading_schedule.day_number,
-
date: check_in.reading_schedule.date,
-
reading_progress: check_in.reading_schedule.reading_progress
-
},
-
content: check_in.content,
-
word_count: check_in.word_count,
-
status: check_in.status,
-
submitted_at: check_in.submitted_at,
-
updated_at: check_in.updated_at,
-
has_flower: check_in.has_flower?,
-
flower: check_in.flower ? {
-
giver: {
-
id: check_in.flower.giver.id,
-
nickname: check_in.flower.giver.nickname
-
},
-
comment: check_in.flower.comment,
-
created_at: check_in.flower.created_at
-
} : nil
-
}
-
end
-
-
# PUT /api/check_ins/:id
-
def update
-
check_in = CheckIn.find(params[:id])
-
-
# 只能修改自己的打卡
-
unless check_in.user_id == current_user.id
-
return render json: { error: "只能修改自己的打卡" }, status: :forbidden
-
end
-
-
# 如果已经获得小红花,不允许修改
-
if check_in.has_flower?
-
return render json: { error: "已获得小红花的打卡不允许修改" }, status: :unprocessable_entity
-
end
-
-
if check_in.update(check_in_params)
-
render json: {
-
id: check_in.id,
-
content: check_in.content,
-
word_count: check_in.word_count,
-
status: check_in.status,
-
updated_at: check_in.updated_at
-
}
-
else
-
render json: { errors: check_in.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def check_in_params
-
params.require(:check_in).permit(:content, :status)
-
end
-
end
-
module Api
-
class CommentsController < Api::ApplicationController
-
include Commentable
-
-
before_action :set_post, only: [:index, :create]
-
-
private
-
-
def fetch_comments
-
@post.comments
-
end
-
-
def build_comment(params)
-
comment = @post.comments.new(params)
-
# 设置 commentable 关联
-
comment.commentable = @post
-
comment
-
end
-
-
def set_post
-
@post = Post.find(params[:post_id])
-
rescue ActiveRecord::RecordNotFound
-
render_not_found('帖子不存在')
-
end
-
end
-
end
-
class Api::DailyLeadingsController < ApplicationController
-
include Authenticable
-
-
skip_before_action :authenticate_user!, only: [:show]
-
-
# POST /api/reading_schedules/:reading_schedule_id/daily_leading
-
def create
-
reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
-
event = reading_schedule.reading_event
-
-
# 检查活动是否进行中
-
unless event.in_progress?
-
return render json: { error: "活动未开始或已结束" }, status: :unprocessable_entity
-
end
-
-
# 检查是否有权限发布领读内容(前一天权限 + 小组长补位)
-
unless event.can_publish_leading_content?(current_user, reading_schedule)
-
return render json: {
-
error: "只有领读人或小组长可以发布领读内容",
-
details: {
-
leader_permission: "领读人可提前一天或当天发布",
-
group_leader_permission: "小组长全程具备发布权限(补位机制)",
-
current_user_role: event.current_leader?(current_user) ? "小组长" : "普通用户",
-
is_schedule_leader: reading_schedule.daily_leader_id == current_user.id
-
}
-
}, status: :forbidden
-
end
-
-
daily_leading = reading_schedule.build_daily_leading(daily_leading_params)
-
daily_leading.leader = current_user
-
-
if daily_leading.save
-
render json: {
-
id: daily_leading.id,
-
leader: {
-
id: daily_leading.leader.id,
-
nickname: daily_leading.leader.nickname,
-
avatar_url: daily_leading.leader.avatar_url
-
},
-
reading_suggestion: daily_leading.reading_suggestion,
-
questions: daily_leading.questions,
-
schedule_date: reading_schedule.date,
-
publish_window: "可提前一天或当天发布",
-
created_at: daily_leading.created_at
-
}, status: :created
-
else
-
render json: { error: daily_leading.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# GET /api/reading_schedules/:reading_schedule_id/daily_leading
-
def show
-
reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
-
daily_leading = reading_schedule.daily_leading
-
-
if daily_leading
-
render json: {
-
id: daily_leading.id,
-
leader: {
-
id: daily_leading.leader.id,
-
nickname: daily_leading.leader.nickname,
-
avatar_url: daily_leading.leader.avatar_url
-
},
-
reading_suggestion: daily_leading.reading_suggestion,
-
questions: daily_leading.questions,
-
created_at: daily_leading.created_at
-
}
-
else
-
render json: { error: "今日暂无领读内容" }, status: :not_found
-
end
-
end
-
-
# PUT /api/reading_schedules/:reading_schedule_id/daily_leading
-
def update
-
reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
-
daily_leading = reading_schedule.daily_leading
-
event = reading_schedule.reading_event
-
-
unless daily_leading
-
return render json: { error: "领读内容不存在" }, status: :not_found
-
end
-
-
# 检查活动是否进行中
-
unless event.in_progress?
-
return render json: { error: "活动未开始或已结束" }, status: :unprocessable_entity
-
end
-
-
# 检查权限:原作者或当前有效的小组长
-
is_original_author = daily_leading.leader_id == current_user.id
-
is_current_leader = event.current_leader?(current_user)
-
is_today_leader = event.current_daily_leader?(current_user, reading_schedule)
-
-
unless is_original_author || is_current_leader || is_today_leader
-
return render json: { error: "无权限修改该内容" }, status: :forbidden
-
end
-
-
# 如果当天已经有打卡,不建议修改领读内容
-
if reading_schedule.check_ins.any?
-
return render json: { error: "已有用户打卡,不建议修改领读内容" }, status: :unprocessable_entity
-
end
-
-
if daily_leading.update(daily_leading_params)
-
render json: {
-
id: daily_leading.id,
-
leader: {
-
id: daily_leading.leader.id,
-
nickname: daily_leading.leader.nickname,
-
avatar_url: daily_leading.leader.avatar_url
-
},
-
reading_suggestion: daily_leading.reading_suggestion,
-
questions: daily_leading.questions,
-
updated_at: daily_leading.updated_at
-
}
-
else
-
render json: { error: daily_leading.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def daily_leading_params
-
params.require(:daily_leading).permit(:reading_suggestion, :questions)
-
end
-
end
-
module Api
-
class EventsController < Api::ApplicationController
-
skip_before_action :authenticate_user!, only: [:index, :show], raise: false
-
include AdminAuthorizable
-
-
# GET /api/events
-
def index
-
@events = ReadingEvent.includes(:leader).order(created_at: :desc)
-
-
# 筛选条件
-
@events = @events.where(status: params[:status]) if params[:status].present?
-
-
render json: @events.map { |event| event_json(event) }
-
end
-
-
# GET /api/events/:id
-
def show
-
@event = ReadingEvent.includes(:leader, :participants).find(params[:id])
-
-
render json: event_detail_json(@event)
-
end
-
-
# POST /api/events
-
def create
-
@event = current_user.created_events.new(event_params)
-
@event.leader = current_user
-
@event.approval_status = :pending # 新活动默认待审批
-
-
if @event.save
-
# 自动生成阅读计划
-
generate_reading_schedules(@event)
-
-
render json: event_json(@event), status: :created
-
else
-
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# PUT /api/events/:id
-
def update
-
@event = current_user.created_events.find(params[:id])
-
-
if @event.update(event_params)
-
render json: event_json(@event)
-
else
-
render json: { errors: @event.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# DELETE /api/events/:id
-
def destroy
-
@event = current_user.created_events.find(params[:id])
-
@event.destroy
-
-
head :no_content
-
end
-
-
# POST /api/events/:id/enroll
-
def enroll
-
@event = ReadingEvent.find(params[:id])
-
-
# 使用EventEnrollmentService处理报名
-
service_result = EventEnrollmentService.enroll_user!(@event, current_user)
-
-
if service_result.success?
-
render json: service_result.result, status: :created
-
else
-
render json: { error: service_result.first_error }, status: :unprocessable_entity
-
end
-
end
-
-
# POST /api/events/:id/approve
-
def approve
-
@event = ReadingEvent.find(params[:id])
-
-
# 检查是否有审批权限
-
authorize_event_approval!
-
-
# 使用EventManagementService处理审批
-
service_result = EventManagementService.approve_event!(@event, current_user)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
render json: { error: service_result.first_error }, status: :unprocessable_entity
-
end
-
end
-
-
# POST /api/events/:id/reject
-
def reject
-
@event = ReadingEvent.find(params[:id])
-
-
# 检查是否有审批权限
-
authorize_event_approval!
-
-
# 使用EventManagementService处理拒绝
-
service_result = EventManagementService.reject_event!(@event, current_user)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
render json: { error: service_result.first_error }, status: :unprocessable_entity
-
end
-
end
-
-
# POST /api/events/:id/claim_leadership
-
def claim_leadership
-
@event = ReadingEvent.find(params[:id])
-
schedule = @event.reading_schedules.find(params[:schedule_id])
-
-
# 使用LeaderAssignmentService处理领读报名
-
service_result = LeaderAssignmentService.claim_leadership!(@event, current_user, schedule)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
render json: { error: service_result.first_error }, status: :unprocessable_entity
-
end
-
end
-
-
# POST /api/events/:id/complete
-
def complete
-
@event = ReadingEvent.find(params[:id])
-
-
# 使用EventManagementService处理活动完成
-
service_result = EventManagementService.complete_event!(@event, current_user)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
status_code = service_result.first_error.include?("只有") ? :forbidden : :unprocessable_entity
-
render json: { error: service_result.first_error }, status: status_code
-
end
-
end
-
-
# GET /api/events/:id/backup_needed
-
def backup_needed
-
@event = ReadingEvent.find(params[:id])
-
-
# 检查是否是当前有效的小组长
-
unless @event.current_leader?(current_user)
-
return render json: { error: "只有活动小组长可以查看补位信息" }, status: :forbidden
-
end
-
-
backup_schedules = @event.schedules_need_backup
-
-
render json: {
-
event_id: @event.id,
-
event_title: @event.title,
-
backup_needed: backup_schedules,
-
summary: {
-
total_needing_backup: backup_schedules.count,
-
missing_content_count: backup_schedules.count { |s| s[:missing_content] },
-
missing_flowers_count: backup_schedules.count { |s| s[:missing_flowers] },
-
urgent_count: backup_schedules.count { |s| s[:date] <= Date.today }
-
},
-
leader_permissions: {
-
can_publish_content: true,
-
can_give_flowers: true,
-
backup_mechanism: "小组长全程具备领读权限,可随时补位"
-
}
-
}
-
end
-
-
private
-
-
def event_params
-
params.require(:event).permit(
-
:title, :book_name, :book_cover_url, :description,
-
:start_date, :end_date, :max_participants, :enrollment_fee, :status,
-
:leader_assignment_type
-
)
-
end
-
-
def event_json(event)
-
{
-
id: event.id,
-
title: event.title,
-
book_name: event.book_name,
-
book_cover_url: event.book_cover_url,
-
description: event.description,
-
start_date: event.start_date,
-
end_date: event.end_date,
-
max_participants: event.max_participants,
-
enrollment_fee: event.enrollment_fee,
-
service_fee: event.service_fee,
-
deposit: event.deposit,
-
status: event.status,
-
approval_status: event.approval_status,
-
leader_assignment_type: event.leader_assignment_type,
-
days_count: event.days_count,
-
leader: {
-
id: event.leader.id,
-
nickname: event.leader.nickname,
-
avatar_url: event.leader.avatar_url
-
},
-
approved_by: event.approved_by ? {
-
id: event.approved_by.id,
-
nickname: event.approved_by.nickname
-
} : nil,
-
approved_at: event.approved_at,
-
created_at: event.created_at
-
}
-
end
-
-
def event_detail_json(event)
-
event_json(event).merge(
-
participants_count: event.participants.count,
-
participants: event.participants.map { |user|
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
}
-
}
-
)
-
end
-
-
# enrollment_json方法已移至EventEnrollmentService
-
-
def generate_reading_schedules(event)
-
days_count = event.days_count
-
-
days_count.times do |i|
-
event.reading_schedules.create!(
-
day_number: i + 1,
-
date: event.start_date + i.days,
-
reading_progress: "第 #{i + 1} 天阅读计划(待领读人填写)"
-
)
-
end
-
end
-
end
-
end
-
class Api::FlowersController < ApplicationController
-
include Authenticable
-
-
# POST /api/check_ins/:check_in_id/flower
-
def create
-
check_in = CheckIn.find(params[:check_in_id])
-
reading_schedule = check_in.reading_schedule
-
event = reading_schedule.reading_event
-
-
# 不能给自己的打卡送花
-
if check_in.user_id == current_user.id
-
return render json: { error: "不能给自己送小红花" }, status: :unprocessable_entity
-
end
-
-
# 检查是否有权限发放小红花(当天和后一天权限 + 小组长补位)
-
unless event.can_give_flowers?(current_user, reading_schedule)
-
return render json: {
-
error: "只有领读人或小组长可以发放小红花",
-
details: {
-
leader_permission: "领读人可在当天或后一天发放",
-
group_leader_permission: "小组长全程具备发放权限(补位机制)",
-
current_user_role: event.current_leader?(current_user) ? "小组长" : "普通用户",
-
flower_window: "小红花发放窗口灵活,支持补位机制"
-
}
-
}, status: :forbidden
-
end
-
-
flower = Flower.new(flower_params)
-
flower.check_in = check_in
-
flower.giver = current_user
-
flower.recipient = check_in.user
-
flower.reading_schedule = reading_schedule
-
-
if flower.save
-
render json: {
-
id: flower.id,
-
check_in_id: flower.check_in_id,
-
giver: {
-
id: flower.giver.id,
-
nickname: flower.giver.nickname,
-
avatar_url: flower.giver.avatar_url
-
},
-
recipient: {
-
id: flower.recipient.id,
-
nickname: flower.recipient.nickname,
-
avatar_url: flower.recipient.avatar_url
-
},
-
comment: flower.comment,
-
flower_window: "领读人可在当天或后一天发放小红花",
-
created_at: flower.created_at
-
}, status: :created
-
else
-
render json: { error: flower.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# GET /api/reading_schedules/:reading_schedule_id/flowers
-
def index
-
reading_schedule = ReadingSchedule.find(params[:reading_schedule_id])
-
flowers = reading_schedule.flowers.includes(:giver, :recipient, :check_in)
-
-
render json: flowers.map { |flower|
-
{
-
id: flower.id,
-
giver: {
-
id: flower.giver.id,
-
nickname: flower.giver.nickname,
-
avatar_url: flower.giver.avatar_url
-
},
-
recipient: {
-
id: flower.recipient.id,
-
nickname: flower.recipient.nickname,
-
avatar_url: flower.recipient.avatar_url
-
},
-
check_in: {
-
id: flower.check_in.id,
-
content: flower.check_in.content.truncate(100)
-
},
-
comment: flower.comment,
-
created_at: flower.created_at
-
}
-
}
-
end
-
-
# GET /api/users/:user_id/flowers
-
def user_flowers
-
user = User.find(params[:user_id])
-
flowers = Flower.where(recipient: user).includes(:giver, :reading_schedule, :check_in)
-
-
render json: {
-
total_count: flowers.count,
-
flowers: flowers.map { |flower|
-
{
-
id: flower.id,
-
giver: {
-
id: flower.giver.id,
-
nickname: flower.giver.nickname,
-
avatar_url: flower.giver.avatar_url
-
},
-
reading_schedule: {
-
id: flower.reading_schedule.id,
-
day_number: flower.reading_schedule.day_number,
-
date: flower.reading_schedule.date
-
},
-
check_in: {
-
id: flower.check_in.id,
-
content: flower.check_in.content.truncate(100)
-
},
-
comment: flower.comment,
-
created_at: flower.created_at
-
}
-
}
-
}
-
end
-
-
private
-
-
def flower_params
-
params.require(:flower).permit(:comment)
-
end
-
end
-
module Api
-
class LikesController < Api::ApplicationController
-
before_action :authenticate_user!
-
-
# POST /api/posts/:post_id/like
-
def create
-
target = find_target
-
-
if target.nil?
-
return render json: { error: '目标不存在' }, status: :not_found
-
end
-
-
if Like.like!(current_user, target)
-
render json: {
-
message: '点赞成功',
-
liked: true,
-
likes_count: target.likes_count
-
}
-
else
-
render json: { error: '已经点赞过了' }, status: :unprocessable_entity
-
end
-
end
-
-
# DELETE /api/posts/:post_id/like
-
def destroy
-
target = find_target
-
-
if target.nil?
-
return render json: { error: '目标不存在' }, status: :not_found
-
end
-
-
if Like.unlike!(current_user, target)
-
render json: {
-
message: '取消点赞成功',
-
liked: false,
-
likes_count: target.likes_count
-
}
-
else
-
render json: { error: '还未点赞' }, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def find_target
-
case params[:post_id]
-
when nil
-
nil
-
else
-
Post.find(params[:post_id])
-
end
-
rescue ActiveRecord::RecordNotFound
-
nil
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
module Api
-
# 优化版本的PostsController - 解决N+1查询问题
-
class OptimizedPostsController < Api::ApplicationController
-
before_action :authenticate_user!
-
include AdminAuthorizable
-
-
# GET /api/posts
-
def index
-
# 基础查询,预加载所有必要的关联
-
@posts = base_posts_query
-
-
# 按分类筛选
-
if params[:category].present?
-
@posts = @posts.by_category(params[:category])
-
end
-
-
# 分页处理
-
@posts = paginate_posts(@posts)
-
-
# 批量预加载权限信息
-
preload_permissions(@posts, current_user) if current_user
-
-
# 批量预加载点赞状态
-
preload_like_status(@posts, current_user) if current_user
-
-
render json: optimized_posts_json(@posts)
-
end
-
-
# GET /api/posts/:id
-
def show
-
@post = Post.find(params[:id])
-
-
# 检查权限:普通用户看不到隐藏帖子
-
unless current_user.any_admin?
-
if @post.hidden?
-
return render json: { error: "帖子已被隐藏" }, status: :not_found
-
end
-
end
-
-
render json: optimized_post_json(@post)
-
end
-
-
private
-
-
# 基础帖子查询
-
def base_posts_query
-
# 如果是管理员,可以看到所有帖子
-
if current_user.any_admin?
-
Post.includes(:user)
-
.order(pinned: :desc, created_at: :desc)
-
else
-
Post.visible.includes(:user)
-
.order(pinned: :desc, created_at: :desc)
-
end
-
end
-
-
# 分页处理
-
def paginate_posts(query)
-
page = params[:page].to_i > 0 ? params[:page].to_i : 1
-
per_page = params[:per_page].to_i > 0 ? [params[:per_page].to_i, 50].min : 20
-
-
@total_count = query.count
-
query.limit(per_page).offset((page - 1) * per_page)
-
end
-
-
# 批量预加载权限信息
-
def preload_permissions(posts, user)
-
post_ids = posts.map(&:id)
-
-
# 批量获取权限信息
-
permissions = PostPermissionService.batch_check_posts_permissions(
-
post_ids,
-
user.id,
-
[:edit, :delete, :pin, :hide, :comment]
-
)
-
-
# 将权限信息附加到每个post对象
-
posts.each do |post|
-
post_id = post.id
-
post.instance_variable_set(:@permissions, {
-
can_edit: permissions.dig(:edit, post_id) || false,
-
can_delete: permissions.dig(:delete, post_id) || false,
-
can_pin: permissions.dig(:pin, post_id) || false,
-
can_hide: permissions.dig(:hide, post_id) || false,
-
can_comment: permissions.dig(:comment, post_id) || false
-
})
-
end
-
end
-
-
# 批量预加载点赞状态
-
def preload_like_status(posts, user)
-
post_ids = posts.map(&:id)
-
-
# 一次性查询用户对所有帖子的点赞状态
-
liked_post_ids = Like.where(
-
user_id: user.id,
-
target_type: 'Post',
-
target_id: post_ids
-
).pluck(:target_id)
-
-
# 将点赞状态附加到每个post对象
-
posts.each do |post|
-
post.instance_variable_set(:@current_user_liked, liked_post_ids.include?(post.id))
-
end
-
end
-
-
# 优化的帖子JSON序列化
-
def optimized_posts_json(posts)
-
{
-
posts: posts.map { |post| optimized_post_json(post, lite: true) },
-
pagination: {
-
current_page: params[:page].to_i > 0 ? params[:page].to_i : 1,
-
per_page: params[:per_page].to_i > 0 ? [params[:per_page].to_i, 50].min : 20,
-
total_count: @total_count,
-
total_pages: (@total_count.to_f / [params[:per_page].to_i, 50].min).ceil,
-
has_next: (params[:page].to_i > 0 ? params[:page].to_i : 1) * [params[:per_page].to_i, 50].min < @total_count,
-
has_prev: (params[:page].to_i > 0 ? params[:page].to_i : 1) > 1
-
}
-
}
-
end
-
-
# 优化的单个帖子JSON序列化
-
def optimized_post_json(post, lite: false)
-
permissions = post.instance_variable_get(:@permissions) || {}
-
liked_status = post.instance_variable_get(:@current_user_liked)
-
-
result = {
-
id: post.id,
-
title: post.title,
-
content: post.content,
-
category: post.category,
-
category_name: post.category_name,
-
pinned: post.pinned,
-
hidden: post.hidden,
-
created_at: post.created_at,
-
updated_at: post.updated_at,
-
time_ago: post.time_ago_in_words(post.created_at),
-
stats: {
-
likes_count: post.likes_count,
-
comments_count: post.comments_count
-
},
-
author: post.user.as_json_for_api
-
}
-
-
# 添加当前用户的交互状态(仅在需要时)
-
if current_user && !lite
-
result[:interactions] = {
-
liked: liked_status || post.liked_by?(current_user),
-
can_edit: permissions[:can_edit] || post.can_edit?(current_user),
-
can_delete: permissions[:can_delete] || post.can_delete?(current_user),
-
can_pin: permissions[:can_pin] || post.can_pin?(current_user),
-
can_hide: permissions[:can_hide] || post.can_hide?(current_user),
-
can_comment: permissions[:can_comment] || post.can_comment?(current_user)
-
}
-
end
-
-
# 包含关联数据(仅在详情页面)
-
if !lite && params[:include_comments] == 'true'
-
result[:recent_comments] = post.comments.limit(5).includes(:user).map(&:as_json_for_api)
-
end
-
-
if !lite && params[:include_likes] == 'true'
-
result[:recent_likes] = post.likes.limit(10).includes(:user).map do |like|
-
{
-
id: like.id,
-
user: like.user.as_json_for_api,
-
created_at: like.created_at
-
}
-
end
-
end
-
-
result
-
end
-
-
def post_params
-
params.require(:post).permit(:title, :content, :category, :images, tags: [])
-
end
-
end
-
end
-
module Api
-
class PostsController < Api::ApplicationController
-
before_action :authenticate_user!
-
include AdminAuthorizable
-
-
# GET /api/posts
-
def index
-
@posts = Post.visible.includes(:user).pinned_first
-
-
# 如果是管理员,可以看到所有帖子
-
if current_user.any_admin?
-
@posts = Post.includes(:user).pinned_first
-
end
-
-
# 按分类筛选
-
if params[:category].present?
-
@posts = @posts.by_category(params[:category])
-
end
-
-
render json: @posts.map { |post|
-
post.instance_variable_set(:@can_edit_current_user, post.can_edit?(current_user))
-
post.instance_variable_set(:@current_user, current_user)
-
post.as_json
-
}
-
end
-
-
# GET /api/posts/:id
-
def show
-
@post = Post.find(params[:id])
-
-
# 检查权限:普通用户看不到隐藏帖子
-
unless current_user.any_admin?
-
if @post.hidden?
-
return render json: { error: "帖子已被隐藏" }, status: :not_found
-
end
-
end
-
-
@post.instance_variable_set(:@can_edit_current_user, @post.can_edit?(current_user))
-
@post.instance_variable_set(:@current_user, current_user)
-
render json: @post.as_json
-
end
-
-
# POST /api/posts
-
def create
-
# 使用PostManagementService处理帖子创建
-
service_result = PostManagementService.create_post!(current_user, post_params)
-
-
if service_result.success?
-
render json: service_result.result, status: :created
-
else
-
render json: { errors: service_result.error_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# PUT /api/posts/:id
-
def update
-
@post = Post.find(params[:id])
-
-
# 使用PostManagementService处理帖子更新
-
service_result = PostManagementService.update_post!(@post, current_user, post_params)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
error_message = service_result.first_error
-
status_code = error_message.include?("权限") ? :forbidden : :unprocessable_entity
-
-
# 对于验证错误,使用errors格式;对于权限错误,使用error格式
-
if service_result.error_messages.any? && service_result.error_messages.first.include?("can't be blank")
-
render json: { errors: service_result.error_messages }, status: status_code
-
else
-
render json: { error: error_message }, status: status_code
-
end
-
end
-
end
-
-
# DELETE /api/posts/:id
-
def destroy
-
@post = Post.find(params[:id])
-
-
# 使用PostManagementService处理帖子删除
-
service_result = PostManagementService.delete_post!(@post, current_user)
-
-
if service_result.success?
-
head :no_content
-
else
-
error_message = service_result.first_error
-
status_code = error_message.include?("权限") ? :forbidden : :unprocessable_entity
-
render json: { error: error_message }, status: status_code
-
end
-
end
-
-
# POST /api/posts/:id/pin # 置顶帖子
-
def pin
-
authenticate_admin! and return
-
-
@post = Post.find(params[:id])
-
-
# 使用PostManagementService处理帖子置顶
-
service_result = PostManagementService.pin_post!(@post, current_user)
-
-
if service_result.success?
-
render json: service_result.result
-
else
-
render json: { error: service_result.first_error }, status: :forbidden
-
end
-
end
-
-
# POST /api/posts/:id/unpin # 取消置顶
-
def unpin
-
authenticate_admin! and return
-
-
@post = Post.find(params[:id])
-
-
unless @post.can_pin?(current_user)
-
render json: { error: "无权限取消置顶此帖子" }, status: :forbidden
-
return
-
end
-
-
@post.unpin!
-
render json: {
-
message: "帖子已取消置顶",
-
post: @post.as_json
-
}
-
end
-
-
# POST /api/posts/:id/hide # 隐藏帖子
-
def hide
-
authenticate_admin! and return
-
-
@post = Post.find(params[:id])
-
-
unless @post.can_hide?(current_user)
-
render json: { error: "无权限隐藏此帖子" }, status: :forbidden
-
return
-
end
-
-
@post.hide!
-
render json: {
-
message: "帖子已隐藏",
-
post: @post.as_json
-
}
-
end
-
-
# POST /api/posts/:id/unhide # 显示帖子
-
def unhide
-
authenticate_admin! and return
-
-
@post = Post.find(params[:id])
-
-
unless @post.can_hide?(current_user)
-
render json: { error: "无权限显示此帖子" }, status: :forbidden
-
return
-
end
-
-
@post.unhide!
-
render json: {
-
message: "帖子已显示",
-
post: @post.as_json
-
}
-
end
-
-
# POST /api/posts/:id/like # 点赞帖子
-
def like
-
@post = Post.find(params[:id])
-
-
if Like.like!(current_user, @post)
-
render json: {
-
message: "点赞成功",
-
liked: true,
-
likes_count: @post.likes_count
-
}
-
else
-
render json: { error: "已经点赞过了" }, status: :unprocessable_entity
-
end
-
end
-
-
# DELETE /api/posts/:id/like # 取消点赞
-
def unlike
-
@post = Post.find(params[:id])
-
-
if Like.unlike!(current_user, @post)
-
render json: {
-
message: "取消点赞成功",
-
liked: false,
-
likes_count: @post.likes_count
-
}
-
else
-
render json: { error: "还未点赞" }, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def post_params
-
params.require(:post).permit(:title, :content, :category, :images, tags: [])
-
end
-
end
-
end
-
module Api
-
class UploadsController < Api::ApplicationController
-
before_action :authenticate_user!
-
-
# POST /api/upload/image
-
def create
-
return render json: { error: '请选择图片文件' }, status: :bad_request unless params[:file]
-
-
uploaded_file = params[:file]
-
-
# 验证文件类型
-
unless uploaded_file.content_type.in?(['image/jpeg', 'image/jpg', 'image/png', 'image/gif'])
-
return render json: { error: '只支持 JPG、PNG、GIF 格式的图片' }, status: :bad_request
-
end
-
-
# 验证文件大小(最大5MB)
-
if uploaded_file.size > 5.megabytes
-
return render json: { error: '图片大小不能超过5MB' }, status: :bad_request
-
end
-
-
begin
-
# 生成唯一文件名
-
file_name = "#{SecureRandom.uuid}_#{uploaded_file.original_filename}"
-
-
# 这里应该将文件存储到云存储服务,如阿里云OSS、腾讯云COS等
-
# 暂时存储到本地,生产环境需要使用云存储
-
file_path = Rails.root.join('tmp', 'uploads', file_name)
-
FileUtils.mkdir_p(File.dirname(file_path))
-
-
File.open(file_path, 'wb') do |file|
-
file.write(uploaded_file.read)
-
end
-
-
# 生成访问URL(开发环境使用本地路径)
-
url = "/uploads/#{file_name}"
-
-
render json: {
-
message: '图片上传成功',
-
url: url,
-
file_name: file_name
-
}
-
-
rescue => e
-
Rails.logger.error "图片上传失败: #{e.message}"
-
render json: { error: '图片上传失败,请重试' }, status: :internal_server_error
-
end
-
end
-
-
private
-
-
def authenticate_user!
-
return head :unauthorized unless current_user
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
class Api::V1::AnalyticsController < ApplicationController
-
before_action :authenticate_user!
-
before_action :require_admin_for_system_analytics
-
-
# GET /api/v1/analytics/overview
-
# 获取系统总览统计(管理员)
-
def overview
-
render json: {
-
success: true,
-
data: AnalyticsService.system_overview,
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/dashboard
-
# 获取用户仪表板数据
-
def dashboard
-
days = params[:days]&.to_i || 30
-
user_analytics = AnalyticsService.user_analytics(current_user, days)
-
-
render json: {
-
success: true,
-
data: user_analytics,
-
period: "#{days} 天",
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/events/:id
-
# 获取活动详细统计
-
def event_stats
-
event = ReadingEvent.find(params[:id])
-
-
# 检查权限:活动创建者、管理员或参与者可以查看统计
-
unless can_view_event_analytics?(event)
-
return render json: {
-
success: false,
-
error: '无权限查看此活动统计'
-
}, status: :forbidden
-
end
-
-
analytics = AnalyticsService.event_analytics(event)
-
-
render json: {
-
success: true,
-
data: analytics,
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/trends
-
# 获取趋势数据
-
def trends
-
metric = params[:metric]&.to_sym
-
period = params[:period]&.to_sym || :week
-
days = params[:days]&.to_i || 30
-
-
unless [:check_ins, :flowers, :users, :events, :notifications].include?(metric)
-
return render json: {
-
success: false,
-
error: '无效的指标类型。支持的类型: check_ins, flowers, users, events, notifications'
-
}, status: :bad_request
-
end
-
-
unless [:day, :week, :month].include?(period)
-
return render json: {
-
success: false,
-
error: '无效的时间周期。支持的周期: day, week, month'
-
}, status: :bad_request
-
end
-
-
trend_data = AnalyticsService.trend_data(metric, period, days)
-
-
render json: {
-
success: true,
-
metric: metric,
-
period: period,
-
days: days,
-
data: trend_data,
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/leaderboards
-
# 获取排行榜数据
-
def leaderboards
-
type = params[:type]&.to_sym || :flowers
-
limit = params[:limit]&.to_i || 10
-
period = params[:period]&.to_sym || :all_time
-
-
unless [:flowers, :check_ins, :participation].include?(type)
-
return render json: {
-
success: false,
-
error: '无效的排行榜类型。支持的类型: flowers, check_ins, participation'
-
}, status: :bad_request
-
end
-
-
unless [:today, :week, :month, :all_time].include?(period)
-
return render json: {
-
success: false,
-
error: '无效的时间周期。支持的周期: today, week, month, all_time'
-
}, status: :bad_request
-
end
-
-
leaderboard_data = AnalyticsService.leaderboards(type, limit, period)
-
-
render json: {
-
success: true,
-
type: type,
-
period: period,
-
limit: limit,
-
data: leaderboard_data,
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/users/:id
-
# 获取用户详细统计(管理员功能)
-
def user_stats
-
user = User.find(params[:id])
-
days = params[:days]&.to_i || 30
-
-
unless current_user.any_admin? || current_user.id == user.id
-
return render json: {
-
success: false,
-
error: '无权限查看此用户统计'
-
}, status: :forbidden
-
end
-
-
analytics = AnalyticsService.user_analytics(user, days)
-
-
render json: {
-
success: true,
-
data: analytics,
-
period: "#{days} 天",
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/summary
-
# 获取简化的统计数据摘要
-
def summary
-
summary_data = {
-
system: {
-
total_users: User.count,
-
active_events: ReadingEvent.where(status: ['enrolling', 'in_progress']).count,
-
today_check_ins: CheckIn.where('created_at >= ?', Date.current).count,
-
today_flowers: Flower.where('created_at >= ?', Date.current).count
-
},
-
user: {
-
enrolled_events: current_user.event_enrollments.where(status: 'enrolled').count,
-
my_check_ins: current_user.check_ins.where('created_at >= ?', 7.days.ago).count,
-
flowers_received: current_user.received_flowers.where('created_at >= ?', 7.days.ago).count,
-
notifications_unread: current_user.received_notifications.unread.count
-
}
-
}
-
-
render json: {
-
success: true,
-
data: summary_data,
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/reports
-
# 生成报告(管理员功能)
-
def reports
-
report_type = params[:type]&.to_sym
-
format = params[:format]&.to_sym || :json
-
-
unless current_user.any_admin?
-
return render json: {
-
success: false,
-
error: '需要管理员权限'
-
}, status: :forbidden
-
end
-
-
case report_type
-
when :monthly
-
report = generate_monthly_report
-
when :activity
-
report = generate_activity_report
-
when :engagement
-
report = generate_engagement_report
-
else
-
return render json: {
-
success: false,
-
error: '无效的报告类型。支持的类型: monthly, activity, engagement'
-
}, status: :bad_request
-
end
-
-
render json: {
-
success: true,
-
report_type: report_type,
-
data: report,
-
generated_at: Time.current
-
}
-
end
-
-
# GET /api/v1/analytics/export
-
# 导出数据(管理员功能)
-
def export
-
export_type = params[:type]&.to_sym
-
-
unless current_user.any_admin?
-
return render json: {
-
success: false,
-
error: '需要管理员权限'
-
}, status: :forbidden
-
end
-
-
case export_type
-
when :users
-
data = export_users_data
-
when :events
-
data = export_events_data
-
when :flowers
-
data = export_flowers_data
-
else
-
return render json: {
-
success: false,
-
error: '无效的导出类型。支持的类型: users, events, flowers'
-
}, status: :bad_request
-
end
-
-
render json: {
-
success: true,
-
export_type: export_type,
-
data: data,
-
record_count: data.count,
-
generated_at: Time.current
-
}
-
end
-
-
private
-
-
# 检查是否为系统分析需要管理员权限
-
def require_admin_for_system_analytics
-
return unless [:overview, :reports, :export].include?(action_name.to_sym)
-
-
unless current_user.any_admin?
-
render json: {
-
success: false,
-
error: '需要管理员权限'
-
}, status: :forbidden
-
end
-
end
-
-
# 检查是否可以查看活动分析
-
def can_view_event_analytics?(event)
-
return true if current_user.any_admin?
-
return true if event.leader_id == current_user.id
-
return true if event.participants.include?(current_user)
-
false
-
end
-
-
# 生成月度报告
-
def generate_monthly_report
-
current_month = Date.current.beginning_of_month
-
last_month = current_month - 1.month
-
-
{
-
current_month: {
-
period: current_month.strftime('%Y年%m月'),
-
users: {
-
new: User.where(created_at: current_month..(current_month + 1.month)).count,
-
active: active_users_in_period(current_month, (current_month + 1.month))
-
},
-
events: {
-
created: ReadingEvent.where(created_at: current_month..(current_month + 1.month)).count,
-
completed: ReadingEvent.where(status: 'completed', updated_at: current_month..(current_month + 1.month)).count
-
},
-
engagement: {
-
check_ins: CheckIn.where(created_at: current_month..(current_month + 1.month)).count,
-
flowers: Flower.where(created_at: current_month..(current_month + 1.month)).count
-
}
-
},
-
comparison: {
-
period: last_month.strftime('%Y年%m月'),
-
user_growth: calculate_growth_rate(
-
User.where(created_at: last_month..current_month).count,
-
User.where(created_at: (last_month - 1.month)..last_month).count
-
),
-
engagement_growth: calculate_growth_rate(
-
CheckIn.where(created_at: current_month..(current_month + 1.month)).count,
-
CheckIn.where(created_at: last_month..current_month).count
-
)
-
}
-
}
-
end
-
-
# 生成活动报告
-
def generate_activity_report
-
status = params[:status] || 'all'
-
-
events = ReadingEvent.all
-
events = events.where(status: status) if status != 'all'
-
-
events.map do |event|
-
{
-
id: event.id,
-
title: event.title,
-
status: event.status,
-
participants_count: event.event_enrollments.where(status: 'enrolled').count,
-
check_ins_count: event.check_ins.count,
-
flowers_count: event.flowers_count,
-
completion_rate: AnalyticsService.send(:calculate_event_completion_rate, event.event_enrollments),
-
engagement_score: AnalyticsService.send(:calculate_event_engagement_score, event)
-
}
-
end
-
end
-
-
# 生成参与度报告
-
def generate_engagement_report
-
{
-
user_engagement: user_engagement_metrics,
-
event_engagement: event_engagement_metrics,
-
daily_activity: daily_activity_metrics,
-
trends: engagement_trends
-
}
-
end
-
-
# 导出用户数据
-
def export_users_data
-
User.all.map do |user|
-
{
-
id: user.id,
-
nickname: user.nickname,
-
wx_openid: user.wx_openid,
-
role: user.role_as_string,
-
created_at: user.created_at,
-
last_activity: user.check_ins.maximum(:created_at) || user.created_at,
-
events_count: user.event_enrollments.count,
-
check_ins_count: user.check_ins.count,
-
flowers_given: user.given_flowers.count,
-
flowers_received: user.received_flowers.count
-
}
-
end
-
end
-
-
# 导出活动数据
-
def export_events_data
-
ReadingEvent.all.map do |event|
-
{
-
id: event.id,
-
title: event.title,
-
book_name: event.book_name,
-
leader: event.leader&.nickname,
-
status: event.status,
-
approval_status: event.approval_status,
-
start_date: event.start_date,
-
end_date: event.end_date,
-
max_participants: event.max_participants,
-
enrolled_count: event.event_enrollments.where(status: 'enrolled').count,
-
check_ins_count: event.check_ins.count,
-
flowers_count: event.flowers_count,
-
created_at: event.created_at
-
}
-
end
-
end
-
-
# 导出小红花数据
-
def export_flowers_data
-
Flower.includes(:giver, :recipient, :check_in).all.map do |flower|
-
{
-
id: flower.id,
-
giver: flower.giver&.nickname,
-
recipient: flower.recipient&.nickname,
-
check_in_content: flower.check_in&.content&.truncate(50),
-
amount: flower.amount,
-
flower_type: flower.flower_type,
-
comment: flower.comment,
-
created_at: flower.created_at
-
}
-
end
-
end
-
-
# 辅助方法
-
def active_users_in_period(start_time, end_time)
-
User.joins(:check_ins)
-
.where('check_ins.created_at >= ? AND check_ins.created_at < ?', start_time, end_time)
-
.distinct
-
.count
-
end
-
-
def calculate_growth_rate(current, previous)
-
return 0 if previous == 0
-
((current - previous).to_f / previous * 100).round(2)
-
end
-
-
def user_engagement_metrics
-
{
-
average_check_ins_per_user: CheckIn.count.to_f / [User.count, 1].max,
-
average_flowers_per_user: Flower.count.to_f / [User.count, 1].max,
-
user_retention_rate: AnalyticsService.send(:user_retention_rate)
-
}
-
end
-
-
def event_engagement_metrics
-
{
-
average_participation_rate: calculate_average_participation_rate,
-
average_completion_rate: calculate_average_completion_rate,
-
most_active_events: most_active_events(5)
-
}
-
end
-
-
def daily_activity_metrics
-
(7.days.ago.to_date..Date.current).map do |date|
-
{
-
date: date.strftime('%Y-%m-%d'),
-
check_ins: CheckIn.where(created_at: date.beginning_of_day..date.end_of_day).count,
-
flowers: Flower.where(created_at: date.beginning_of_day..date.end_of_day).count,
-
active_users: active_users_in_period(date.beginning_of_day, date.end_of_day)
-
}
-
end
-
end
-
-
def engagement_trends
-
{
-
check_ins_trend: AnalyticsService.trend_data(:check_ins, :week, 30),
-
flowers_trend: AnalyticsService.trend_data(:flowers, :week, 30),
-
users_trend: AnalyticsService.trend_data(:users, :week, 30)
-
}
-
end
-
-
def calculate_average_participation_rate
-
total_events = ReadingEvent.where(status: ['enrolling', 'in_progress', 'completed']).count
-
return 0 if total_events == 0
-
-
total_capacity = ReadingEvent.where(status: ['enrolling', 'in_progress', 'completed'])
-
.sum(:max_participants)
-
total_enrolled = ReadingEvent.joins(:event_enrollments)
-
.where(event_enrollments: { status: 'enrolled' })
-
.count
-
-
(total_enrolled.to_f / total_capacity * 100).round(2)
-
end
-
-
def calculate_average_completion_rate
-
completed_enrollments = EventEnrollment.where(status: 'completed').count
-
total_enrollments = EventEnrollment.where.not(status: 'cancelled').count
-
return 0 if total_enrollments == 0
-
-
(completed_enrollments.to_f / total_enrollments * 100).round(2)
-
end
-
-
def most_active_events(limit = 5)
-
ReadingEvent.joins(:check_ins)
-
.group('reading_events.id')
-
.select('reading_events.*, COUNT(check_ins.id) as check_ins_count')
-
.order('check_ins_count DESC')
-
.limit(limit)
-
.map do |event|
-
{
-
id: event.id,
-
title: event.title,
-
check_ins_count: event.check_ins_count,
-
flowers_count: event.flowers_count
-
}
-
end
-
end
-
end
-
class Api::V1::ApprovalWorkflowController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :check_admin_permissions
-
-
# POST /api/v1/approval_workflow/submit_for_approval
-
# 提交活动审批
-
def submit_for_approval
-
event_id = params[:event_id]
-
workflow_type = params[:workflow_type]&.to_sym || :standard
-
-
unless event_id.present?
-
render_error(
-
message: '请提供活动ID',
-
code: 'EVENT_ID_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
event = ReadingEvent.find_by(id: event_id)
-
unless event
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限(只有活动创建者可以提交审批)
-
unless event.leader == current_user
-
render_error(
-
message: '只有活动创建者可以提交审批',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
service = ActivityApprovalWorkflowService.submit_for_approval!(event, workflow_type: workflow_type)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('approval_workflow#submit_for_approval')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'SUBMIT_FOR_APPROVAL_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '提交审批失败',
-
errors: [e.message],
-
code: 'SUBMIT_FOR_APPROVAL_ERROR'
-
)
-
end
-
-
# POST /api/v1/approval_workflow/approve_event
-
# 审批通过活动
-
def approve_event
-
event_id = params[:event_id]
-
reason = params[:reason]
-
notes = params[:notes]
-
-
unless event_id.present?
-
render_error(
-
message: '请提供活动ID',
-
code: 'EVENT_ID_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
event = ReadingEvent.find_by(id: event_id)
-
unless event
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
service = ActivityApprovalWorkflowService.approve!(event, current_user, reason: reason, notes: notes)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('approval_workflow#approve_event')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'APPROVE_EVENT_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '审批通过失败',
-
errors: [e.message],
-
code: 'APPROVE_EVENT_ERROR'
-
)
-
end
-
-
# POST /api/v1/approval_workflow/reject_event
-
# 审批拒绝活动
-
def reject_event
-
event_id = params[:event_id]
-
reason = params[:reason]
-
notes = params[:notes]
-
-
unless event_id.present?
-
render_error(
-
message: '请提供活动ID',
-
code: 'EVENT_ID_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
unless reason.present?
-
render_error(
-
message: '请提供拒绝理由',
-
code: 'REJECTION_REASON_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
event = ReadingEvent.find_by(id: event_id)
-
unless event
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
service = ActivityApprovalWorkflowService.reject!(event, current_user, reason, notes: notes)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('approval_workflow#reject_event')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'REJECT_EVENT_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '审批拒绝失败',
-
errors: [e.message],
-
code: 'REJECT_EVENT_ERROR'
-
)
-
end
-
-
# POST /api/v1/approval_workflow/batch_approve
-
# 批量审批通过
-
def batch_approve
-
event_ids = params[:event_ids]
-
reason = params[:reason]
-
-
unless event_ids.present? && event_ids.is_a?(Array)
-
render_error(
-
message: '请提供有效的活动ID列表',
-
code: 'EVENT_IDS_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
service = ActivityApprovalWorkflowService.batch_approve!(event_ids, current_user, reason: reason)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('approval_workflow#batch_approve')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'BATCH_APPROVE_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '批量审批失败',
-
errors: [e.message],
-
code: 'BATCH_APPROVE_ERROR'
-
)
-
end
-
-
# POST /api/v1/approval_workflow/batch_reject
-
# 批量审批拒绝
-
def batch_reject
-
event_ids = params[:event_ids]
-
reason = params[:reason]
-
notes = params[:notes]
-
-
unless event_ids.present? && event_ids.is_a?(Array)
-
render_error(
-
message: '请提供有效的活动ID列表',
-
code: 'EVENT_IDS_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
unless reason.present?
-
render_error(
-
message: '请提供拒绝理由',
-
code: 'REJECTION_REASON_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
service = ActivityApprovalWorkflowService.batch_reject!(event_ids, current_user, reason, notes: notes)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('approval_workflow#batch_reject')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'BATCH_REJECT_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '批量拒绝失败',
-
errors: [e.message],
-
code: 'BATCH_REJECT_ERROR'
-
)
-
end
-
-
# GET /api/v1/approval_workflow/approval_queue
-
# 获取审批队列
-
def approval_queue
-
filters = {
-
page: safe_integer_param(params[:page]) || 1,
-
per_page: safe_integer_param(params[:per_page]) || 20,
-
leader_id: safe_integer_param(params[:leader_id]),
-
activity_mode: params[:activity_mode],
-
fee_type: params[:fee_type],
-
submitted_since: parse_date_param(params[:submitted_since]),
-
submitted_until: parse_date_param(params[:submitted_until])
-
}.compact
-
-
service = ActivityApprovalWorkflowService.approval_queue(current_user, filters: filters)
-
-
if service.success?
-
render_success(
-
data: service.result
-
)
-
log_api_call('approval_workflow#approval_queue')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'GET_APPROVAL_QUEUE_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '获取审批队列失败',
-
errors: [e.message],
-
code: 'GET_APPROVAL_QUEUE_ERROR'
-
)
-
end
-
-
# GET /api/v1/approval_workflow/approval_statistics
-
# 获取审批统计
-
def approval_statistics
-
date_range = nil
-
if params[:start_date].present? && params[:end_date].present?
-
start_date = parse_date_param(params[:start_date])
-
end_date = parse_date_param(params[:end_date])
-
date_range = (start_date..end_date) if start_date && end_date
-
end
-
-
service = ActivityApprovalWorkflowService.approval_statistics(current_user, date_range: date_range)
-
-
if service.success?
-
render_success(
-
data: service.result
-
)
-
log_api_call('approval_workflow#approval_statistics')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'GET_APPROVAL_STATISTICS_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '获取审批统计失败',
-
errors: [e.message],
-
code: 'GET_APPROVAL_STATISTICS_ERROR'
-
)
-
end
-
-
# POST /api/v1/approval_workflow/escalate_approval
-
# 升级审批
-
def escalate_approval
-
event_id = params[:event_id]
-
escalation_reason = params[:escalation_reason]
-
-
unless event_id.present?
-
render_error(
-
message: '请提供活动ID',
-
code: 'EVENT_ID_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
unless escalation_reason.present?
-
render_error(
-
message: '请提供升级理由',
-
code: 'ESCALATION_REASON_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
event = ReadingEvent.find_by(id: event_id)
-
unless event
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
service = ActivityApprovalWorkflowService.escalate!(event, current_user, escalation_reason)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('approval_workflow#escalate_approval')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'ESCALATE_APPROVAL_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '升级审批失败',
-
errors: [e.message],
-
code: 'ESCALATE_APPROVAL_ERROR'
-
)
-
end
-
-
# GET /api/v1/approval_workflow/event_approval_status
-
# 获取活动审批状态
-
def event_approval_status
-
event_id = params[:event_id]
-
-
unless event_id.present?
-
render_error(
-
message: '请提供活动ID',
-
code: 'EVENT_ID_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
event = ReadingEvent.find_by(id: event_id)
-
unless event
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查查看权限
-
unless can_view_event_approval_status?(event)
-
render_error(
-
message: '权限不足,无法查看审批状态',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
approval_status_data = {
-
event_id: event.id,
-
title: event.title,
-
status: event.status,
-
approval_status: event.approval_status,
-
submitted_for_approval_at: event.submitted_for_approval_at,
-
approved_at: event.approved_at,
-
approver: event.approver ? user_info(event.approver) : nil,
-
approval_reason: event.approval_reason,
-
approval_notes: event.approval_notes,
-
rejection_reason: event.rejection_reason,
-
escalated_at: event.escalated_at,
-
escalated_by: event.escalated_by ? user_info(event.escalated_by) : nil,
-
escalation_reason: event.escalation_reason,
-
can_submit_for_approval: event.can_submit_for_approval?,
-
can_resubmit_for_approval: event.can_resubmit_for_approval?,
-
approval_queue_position: get_approval_queue_position(event),
-
validation_status: validate_event_for_approval_display(event)
-
}
-
-
render_success(
-
data: approval_status_data
-
)
-
log_api_call('approval_workflow#event_approval_status')
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '获取审批状态失败',
-
errors: [e.message],
-
code: 'GET_APPROVAL_STATUS_ERROR'
-
)
-
end
-
-
private
-
-
# 检查管理员权限
-
def check_admin_permissions
-
unless current_user.can_approve_events? || current_user.can_view_approval_queue?
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
end
-
end
-
-
# 检查是否可以查看活动审批状态
-
def can_view_event_approval_status?(event)
-
# 活动创建者可以查看
-
return true if event.leader == current_user
-
-
# 管理员可以查看
-
return true if current_user.can_approve_events?
-
-
false
-
end
-
-
# 获取审批队列位置
-
def get_approval_queue_position(event)
-
return nil unless event.pending_approval?
-
-
ReadingEvent.where(approval_status: :pending)
-
.where('submitted_for_approval_at <= ?', event.submitted_for_approval_at)
-
.count
-
end
-
-
# 验证活动审批状态显示
-
def validate_event_for_approval_display(event)
-
validation_result = event.send(:validate_event_for_approval)
-
{
-
valid: validation_result[:valid],
-
errors: validation_result[:errors],
-
missing_fields: get_missing_required_fields(event)
-
}
-
end
-
-
# 获取缺失的必填字段
-
def get_missing_required_fields(event)
-
missing_fields = []
-
-
required_fields = [
-
{ field: :title, name: '活动标题' },
-
{ field: :book_name, name: '书籍名称' },
-
{ field: :description, name: '活动描述' },
-
{ field: :start_date, name: '开始日期' },
-
{ field: :end_date, name: '结束日期' },
-
{ field: :max_participants, name: '最大参与人数' }
-
]
-
-
required_fields.each do |field_config|
-
value = event.send(field_config[:field])
-
if value.blank?
-
missing_fields << {
-
field: field_config[:field],
-
name: field_config[:name],
-
current_value: value
-
}
-
end
-
end
-
-
# 检查费用相关字段(如果是收费活动)
-
if event.fee_type != 'free'
-
fee_fields = [
-
{ field: :fee_amount, name: '费用金额' },
-
{ field: :leader_reward_percentage, name: '领读人奖励比例' }
-
]
-
-
fee_fields.each do |field_config|
-
value = event.send(field_config[:field])
-
if value.blank? || value.to_f <= 0
-
missing_fields << {
-
field: field_config[:field],
-
name: field_config[:name],
-
current_value: value
-
}
-
end
-
end
-
end
-
-
missing_fields
-
end
-
-
# 格式化用户信息
-
def user_info(user)
-
return nil unless user
-
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
}
-
end
-
-
# 安全整数参数转换
-
def safe_integer_param(param)
-
return nil if param.blank?
-
-
Integer(param)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
-
# 解析日期参数
-
def parse_date_param(param)
-
return nil if param.blank?
-
-
Date.parse(param.to_s)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
require 'digest'
-
-
class Api::V1::BaseController < ActionController::API
-
# 添加JSON支持
-
include ActionController::MimeResponds
-
# 添加API版本控制
-
include ApiVersionable
-
# 添加全局错误处理
-
include GlobalErrorHandler
-
# 添加API响应格式化
-
include ApiResponseFormatter
-
# 添加请求验证
-
include RequestValidator
-
# 添加API安全增强
-
include ApiSecurity
-
# 添加用户体验增强
-
include UserExperienceEnhancer
-
-
private
-
-
# 统一成功响应格式 - 使用 ApiResponseService
-
def render_success(data: nil, message: '操作成功', meta: {}, status_code: 200)
-
response, status = ApiResponseService.success_response(
-
data: data,
-
message: message,
-
meta: meta,
-
status_code: status_code
-
)
-
render json: response, status: status
-
end
-
-
# 统一错误响应格式 - 使用 ApiResponseService
-
def render_error(message: '操作失败', error_code: nil, details: {}, status_code: 400)
-
response, status = ApiResponseService.error_response(
-
message: message,
-
error_code: error_code,
-
details: details,
-
status_code: status_code
-
)
-
render json: response, status: status
-
end
-
-
# 验证错误响应
-
def render_validation_error(errors, message: '请求参数验证失败')
-
response, status = ApiResponseService.validation_error_response(errors, message: message)
-
render json: response, status: status
-
end
-
-
# 未找到错误响应
-
def render_not_found(resource_type: '资源', resource_id: nil)
-
response, status = ApiResponseService.not_found_response(
-
resource_type: resource_type,
-
resource_id: resource_id
-
)
-
render json: response, status: status
-
end
-
-
# 权限错误响应
-
def render_authorization_error(message: '权限不足', required_permission: nil)
-
response, status = ApiResponseService.authorization_error_response(
-
message: message,
-
required_permission: required_permission
-
)
-
render json: response, status: status
-
end
-
-
# 认证错误响应
-
def render_authentication_error(message: '认证失败', details: {})
-
response, status = ApiResponseService.authentication_error_response(
-
message: message,
-
details: details
-
)
-
render json: response, status: status
-
end
-
-
# 服务不可用错误响应
-
def render_service_unavailable(service_name: '服务', retry_after: 30)
-
response, status = ApiResponseService.service_unavailable_response(
-
service_name: service_name,
-
retry_after: retry_after
-
)
-
render json: response, status: status
-
end
-
-
# 限流错误响应
-
def render_rate_limit_error(limit_info = {})
-
response, status = ApiResponseService.rate_limit_error_response(limit_info)
-
render json: response, status: status
-
end
-
-
# 分页响应
-
def render_paginated(records:, pagination:, message: '获取成功', additional_meta: {})
-
response, status = ApiResponseService.paginated_response(
-
records: records,
-
pagination: pagination,
-
message: message,
-
additional_meta: additional_meta
-
)
-
render json: response, status: status
-
end
-
-
# 创建成功响应
-
def render_create_success(resource, resource_name: '资源')
-
response, status = ApiResponseService.create_success_response(
-
resource,
-
resource_name: resource_name
-
)
-
render json: response, status: status
-
end
-
-
# 更新成功响应
-
def render_update_success(resource, resource_name: '资源')
-
response, status = ApiResponseService.update_success_response(
-
resource,
-
resource_name: resource_name
-
)
-
render json: response, status: status
-
end
-
-
# 删除成功响应
-
def render_destroy_success(resource_name: '资源')
-
response, status = ApiResponseService.destroy_success_response(
-
resource_name: resource_name
-
)
-
render json: response, status: status
-
end
-
-
# 批量操作响应
-
def render_batch_operation(results, operation_name: '批量操作')
-
response, status = ApiResponseService.batch_operation_response(
-
results,
-
operation_name: operation_name
-
)
-
render json: response, status: status
-
end
-
-
# 健康检查响应
-
def render_health_check(additional_info = {})
-
response, status = ApiResponseService.health_response(additional_info)
-
render json: response, status: status
-
end
-
-
# 当前用户认证
-
def authenticate_user!
-
auth_header = request.headers['Authorization']
-
token = auth_header&.split(' ')&.last
-
-
unless token
-
render_authentication_error(
-
message: '请先登录',
-
details: { reason: 'missing_token', required_format: 'Bearer <token>' }
-
)
-
return false
-
end
-
-
decoded = User.decode_jwt_token(token)
-
unless decoded
-
render_authentication_error(
-
message: '认证令牌无效',
-
details: { reason: 'invalid_token', token_provided: token[0..20] + '...' }
-
)
-
return false
-
end
-
-
@current_user = User.find_by(id: decoded['user_id'])
-
-
unless @current_user
-
render_authentication_error(
-
message: '用户不存在',
-
details: { reason: 'user_not_found', user_id: decoded['user_id'] }
-
)
-
return false
-
end
-
-
true
-
rescue => e
-
Rails.logger.error "Authentication error: #{e.message}"
-
render_authentication_error(
-
message: '认证失败',
-
details: { reason: 'processing_error', error: e.message }
-
)
-
false
-
end
-
-
# 权限检查 - 必须是活动创建者
-
def authorize_event_leader!
-
unless @current_user == @reading_event&.leader
-
render_authorization_error(
-
message: '权限不足,只有活动创建者可以执行此操作',
-
required_permission: 'event_leader'
-
)
-
end
-
end
-
-
# 权限检查 - 必须是管理员
-
def authorize_admin!
-
unless @current_user&.admin?
-
render_authorization_error(
-
message: '权限不足,只有管理员可以执行此操作',
-
required_permission: 'admin_access'
-
)
-
end
-
end
-
-
# 分页参数处理
-
def pagination_params
-
{
-
page: params[:page]&.to_i || 1,
-
per_page: [params[:per_page]&.to_i || 20, 100].min
-
}
-
end
-
-
# 排序参数处理
-
def sorting_params(default_field: :created_at, default_direction: :desc)
-
{
-
sort_field: params[:sort]&.to_sym || default_field,
-
sort_direction: params[:direction]&.to_sym || default_direction
-
}
-
end
-
-
# 构建分页元数据
-
def pagination_meta(collection)
-
{
-
current_page: collection.current_page,
-
per_page: collection.limit_value,
-
total_pages: collection.total_pages,
-
total_count: collection.total_count,
-
next_page: collection.next_page,
-
prev_page: collection.prev_page
-
}
-
end
-
-
# 安全的参数检查
-
def safe_integer_param(param_name, default_value: 0)
-
value = params[param_name]
-
return default_value if value.blank?
-
-
begin
-
value.to_i
-
rescue ArgumentError, TypeError
-
default_value
-
end
-
end
-
-
def safe_decimal_param(param_name, default_value: 0.0)
-
value = params[param_name]
-
return default_value if value.blank?
-
-
begin
-
BigDecimal(value.to_s)
-
rescue ArgumentError, TypeError
-
default_value
-
end
-
end
-
-
def safe_date_param(param_name)
-
value = params[param_name]
-
return nil if value.blank?
-
-
begin
-
Date.parse(value)
-
rescue ArgumentError
-
nil
-
end
-
end
-
-
def safe_datetime_param(param_name)
-
value = params[param_name]
-
return nil if value.blank?
-
-
begin
-
DateTime.parse(value)
-
rescue ArgumentError
-
nil
-
end
-
end
-
-
# 当前用户
-
def current_user
-
@current_user
-
end
-
-
# 记录API调用日志
-
def log_api_call(action, result = 'success')
-
Rails.logger.info "API Call: #{action} by User #{current_user&.id} - #{result}"
-
end
-
-
# 参数验证辅助方法
-
def validate_required_fields(*fields)
-
missing_fields = fields.select { |field| params[field].blank? }
-
-
if missing_fields.any?
-
render_validation_error(
-
missing_fields.map { |field| "#{field} 不能为空" }.to_hash,
-
message: '缺少必要参数'
-
)
-
return false
-
end
-
-
true
-
end
-
end
-
class Api::V1::CheckInsController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :set_check_in, only: [:show, :update, :destroy]
-
before_action :check_check_in_permission, only: [:update, :destroy]
-
-
# POST /api/v1/reading_schedules/:reading_schedule_id/check_ins
-
# 创建打卡
-
def create
-
schedule_id = params[:reading_schedule_id]
-
schedule = ReadingSchedule.find_by(id: schedule_id)
-
-
unless schedule
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查用户是否已报名该活动
-
enrollment = current_user.event_enrollments.find_by(reading_event: schedule.reading_event)
-
unless enrollment
-
render_error(
-
message: '您还未报名该活动',
-
code: 'NOT_ENROLLED',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 检查活动状态
-
unless schedule.reading_event.in_progress?
-
render_error(
-
message: '活动尚未开始或已结束',
-
code: 'EVENT_NOT_ACTIVE',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 检查是否已经打卡
-
existing_check_in = CheckIn.find_by(
-
user: current_user,
-
reading_schedule: schedule
-
)
-
-
if existing_check_in
-
render_error(
-
message: '今日已打卡',
-
code: 'ALREADY_CHECKED_IN',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 检查打卡时间窗口
-
unless can_check_in?(schedule)
-
render_error(
-
message: '打卡时间已过,请使用补卡功能',
-
code: 'CHECK_IN_TIME_EXPIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
check_in = CheckIn.new(check_in_params)
-
check_in.user = current_user
-
check_in.reading_schedule = schedule
-
check_in.enrollment = enrollment
-
-
if check_in.save
-
render_success(
-
data: check_in_response_data(check_in),
-
message: '打卡成功'
-
)
-
log_api_call('check_ins#create')
-
else
-
render_error(
-
message: '打卡失败',
-
errors: check_in.errors.full_messages,
-
code: 'CHECK_IN_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '打卡失败',
-
errors: [e.message],
-
code: 'CHECK_IN_ERROR'
-
)
-
end
-
-
# GET /api/v1/reading_schedules/:reading_schedule_id/check_ins
-
# 获取打卡列表
-
def index
-
schedule_id = params[:reading_schedule_id]
-
schedule = ReadingSchedule.find_by(id: schedule_id)
-
-
unless schedule
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限
-
unless can_view_check_ins?(schedule)
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 分页参数
-
page = safe_integer_param(params[:page]) || 1
-
per_page = safe_integer_param(params[:per_page]) || 20
-
-
check_ins = schedule.check_ins.includes(:user, :flowers, :comments)
-
.order(submitted_at: :desc)
-
.page(page)
-
.per(per_page)
-
-
render_success(
-
data: {
-
check_ins: check_ins.map { |ci| check_in_response_data(ci) },
-
pagination: {
-
current_page: page,
-
per_page: per_page,
-
total_pages: check_ins.total_pages,
-
total_count: check_ins.total_count
-
},
-
schedule_info: schedule_basic_info(schedule),
-
statistics: schedule_check_in_statistics(schedule)
-
}
-
)
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '获取打卡列表失败',
-
errors: [e.message],
-
code: 'GET_CHECK_INS_ERROR'
-
)
-
end
-
-
# GET /api/v1/check_ins/:id
-
# 获取打卡详情
-
def show
-
format_content = params[:format_content] == 'true'
-
-
render_success(
-
data: check_in_response_data(@check_in, detailed: true, format_content: format_content)
-
)
-
rescue => e
-
render_error(
-
message: '获取打卡详情失败',
-
errors: [e.message],
-
code: 'GET_CHECK_IN_ERROR'
-
)
-
end
-
-
# PUT /api/v1/check_ins/:id
-
# 更新打卡
-
def update
-
# 检查打卡是否可以编辑
-
unless @check_in.can_be_edited?
-
render_error(
-
message: '活动已结束,无法编辑打卡',
-
code: 'CANNOT_EDIT',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 检查是否已获得小红花,如果有则给出警告
-
if @check_in.flowers.any?
-
render_error(
-
message: '已获得小红花的打卡修改可能会影响小红花发放者的统计,请谨慎操作。是否继续修改?',
-
code: 'HAS_FLOWERS_WARNING',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
if @check_in.update(check_in_params)
-
render_success(
-
data: check_in_response_data(@check_in),
-
message: '打卡更新成功'
-
)
-
log_api_call('check_ins#update')
-
else
-
render_error(
-
message: '打卡更新失败',
-
errors: @check_in.errors.full_messages,
-
code: 'UPDATE_CHECK_IN_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '打卡更新失败',
-
errors: [e.message],
-
code: 'UPDATE_CHECK_IN_ERROR'
-
)
-
end
-
-
# DELETE /api/v1/check_ins/:id
-
# 删除打卡
-
def destroy
-
# 只能删除自己的打卡
-
unless @check_in.user == current_user
-
render_error(
-
message: '只能删除自己的打卡',
-
code: 'CANNOT_DELETE_OTHERS',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 检查打卡是否可以删除
-
unless @check_in.can_be_deleted?
-
render_error(
-
message: '活动已结束,无法删除打卡',
-
code: 'CANNOT_DELETE',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 检查是否已获得小红花,如果有则给出警告
-
if @check_in.flowers.any?
-
flowers_count = @check_in.flowers.count
-
render_error(
-
message: "该打卡已获得#{flowers_count}朵小红花,删除后将同时删除这些小红花记录,是否确认删除?",
-
code: 'HAS_FLOWERS_WARNING',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
if @check_in.destroy
-
render_success(
-
message: '打卡删除成功,相关统计数据已更新'
-
)
-
log_api_call('check_ins#destroy')
-
else
-
render_error(
-
message: '打卡删除失败',
-
code: 'DELETE_CHECK_IN_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '打卡删除失败',
-
errors: [e.message],
-
code: 'DELETE_CHECK_IN_ERROR'
-
)
-
end
-
-
# POST /api/v1/check_ins/:id/submit_late
-
# 提交迟到打卡
-
def submit_late
-
# 检查是否可以编辑(活动是否已结束)
-
unless @check_in.can_be_edited?
-
render_error(
-
message: '活动已结束,无法提交迟到打卡',
-
code: 'CANNOT_SUBMIT_LATE',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 更新状态为迟到
-
if @check_in.update(status: :late)
-
render_success(
-
data: check_in_response_data(@check_in),
-
message: '迟到打卡提交成功'
-
)
-
log_api_call('check_ins#submit_late')
-
else
-
render_error(
-
message: '迟到打卡提交失败',
-
errors: @check_in.errors.full_messages,
-
code: 'SUBMIT_LATE_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '迟到打卡提交失败',
-
errors: [e.message],
-
code: 'SUBMIT_LATE_ERROR'
-
)
-
end
-
-
# POST /api/v1/check_ins/:id/submit_supplement
-
# 提交补卡
-
def submit_supplement
-
# 检查是否可以编辑(活动是否已结束)
-
unless @check_in.can_be_edited?
-
render_error(
-
message: '活动已结束,无法提交补卡',
-
code: 'CANNOT_MAKEUP',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 检查是否可以补卡(基于日期和活动状态)
-
unless @check_in.can_makeup?
-
render_error(
-
message: '该打卡不适用补卡功能',
-
code: 'CANNOT_MAKEUP',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 更新状态为补卡
-
if @check_in.update(status: :supplement)
-
render_success(
-
data: check_in_response_data(@check_in),
-
message: '补卡提交成功'
-
)
-
log_api_call('check_ins#submit_supplement')
-
else
-
render_error(
-
message: '补卡提交失败',
-
errors: @check_in.errors.full_messages,
-
code: 'SUBMIT_SUPPLEMENT_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '补卡提交失败',
-
errors: [e.message],
-
code: 'SUBMIT_SUPPLEMENT_ERROR'
-
)
-
end
-
-
# GET /api/v1/users/:user_id/check_ins
-
# 获取用户的打卡记录
-
def user_check_ins
-
user_id = safe_integer_param(params[:user_id])
-
user = user_id ? User.find_by(id: user_id) : current_user
-
-
unless user
-
render_error(
-
message: '用户不存在',
-
code: 'USER_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限(只能查看自己的打卡,除非是管理员)
-
unless user == current_user || current_user.can_approve_events?
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 分页参数
-
page = safe_integer_param(params[:page]) || 1
-
per_page = safe_integer_param(params[:per_page]) || 20
-
-
# 筛选参数
-
status_filter = params[:status]
-
start_date = parse_date_param(params[:start_date])
-
end_date = parse_date_param(params[:end_date])
-
-
check_ins = user.check_ins.includes(:reading_schedule, :flowers, :comments)
-
.joins(:reading_schedule)
-
-
# 应用筛选条件
-
check_ins = check_ins.where(status: status_filter) if status_filter.present?
-
check_ins = check_ins.where('reading_schedules.date >= ?', start_date) if start_date.present?
-
check_ins = check_ins.where('reading_schedules.date <= ?', end_date) if end_date.present?
-
-
check_ins = check_ins.order(submitted_at: :desc)
-
.page(page)
-
.per(per_page)
-
-
render_success(
-
data: {
-
check_ins: check_ins.map { |ci| check_in_response_data(ci) },
-
pagination: {
-
current_page: page,
-
per_page: per_page,
-
total_pages: check_ins.total_pages,
-
total_count: check_ins.total_count
-
},
-
user: user_info(user),
-
statistics: user_check_in_statistics(user)
-
}
-
)
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '用户不存在',
-
code: 'USER_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '获取用户打卡记录失败',
-
errors: [e.message],
-
code: 'GET_USER_CHECK_INS_ERROR'
-
)
-
end
-
-
# GET /api/v1/check_ins/statistics
-
# 获取打卡统计
-
def statistics
-
# 统计参数
-
event_id = safe_integer_param(params[:event_id])
-
schedule_id = safe_integer_param(params[:schedule_id])
-
date_range = params[:date_range] # today, week, month
-
-
base_query = CheckIn.includes(:user, :reading_schedule)
-
-
# 应用筛选条件
-
if event_id
-
event = ReadingEvent.find_by(id: event_id)
-
base_query = base_query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: event_id })
-
end
-
-
if schedule_id
-
base_query = base_query.where(reading_schedule_id: schedule_id)
-
end
-
-
case date_range
-
when 'today'
-
base_query = base_query.joins(:reading_schedule).where(reading_schedules: { date: Date.current })
-
when 'week'
-
base_query = base_query.joins(:reading_schedule).where(reading_schedules: { date: Date.current.beginning_of_week..Date.current.end_of_week })
-
when 'month'
-
base_query = base_query.joins(:reading_schedule).where(reading_schedules: { date: Date.current.beginning_of_month..Date.current.end_of_month })
-
end
-
-
total_check_ins = base_query.count
-
normal_check_ins = base_query.where(status: :normal).count
-
supplement_check_ins = base_query.where(status: :supplement).count
-
late_check_ins = base_query.where(status: :late).count
-
-
# 用户统计
-
user_stats = base_query.group(:user_id).count
-
active_users = user_stats.size
-
-
# 内容统计
-
total_words = base_query.sum(:word_count)
-
avg_words = total_check_ins > 0 ? (total_words.to_f / total_check_ins).round(2) : 0
-
-
# 小红花统计
-
flowers_stats = base_query.joins(:flowers).group(:check_in_id).count
-
-
render_success(
-
data: {
-
total_check_ins: total_check_ins,
-
normal_check_ins: normal_check_ins,
-
supplement_check_ins: supplement_check_ins,
-
late_check_ins: late_check_ins,
-
active_users: active_users,
-
total_words: total_words,
-
average_words: avg_words,
-
flowers_given: flowers_stats.size,
-
date_range: date_range,
-
event_id: event_id,
-
schedule_id: schedule_id
-
}
-
)
-
rescue => e
-
render_error(
-
message: '获取打卡统计失败',
-
errors: [e.message],
-
code: 'GET_CHECK_INS_STATISTICS_ERROR'
-
)
-
end
-
-
private
-
-
def set_check_in
-
@check_in = CheckIn.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '打卡记录不存在',
-
code: 'CHECK_IN_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def check_check_in_permission
-
unless @check_in.user == current_user
-
render_error(
-
message: '只能操作自己的打卡',
-
code: 'PERMISSION_DENIED',
-
status: :forbidden
-
)
-
end
-
end
-
-
def can_check_in?(schedule)
-
return true unless schedule # 防止nil错误
-
-
schedule_date = schedule.date
-
current_time = Time.current
-
-
# 当天的打卡可以在晚上11:59前提交
-
if schedule_date == Date.current
-
return current_time <= schedule_date.to_time.end_of_day
-
end
-
-
# 过去的日期可以补卡
-
schedule_date < Date.current && schedule.reading_event.in_progress?
-
end
-
-
def can_view_check_ins?(schedule)
-
# 活动参与者可以查看
-
return true if current_user.enrolled?(schedule.reading_event)
-
-
# 活动创建者可以查看
-
return true if schedule.reading_event.leader == current_user
-
-
# 领读人可以查看
-
if schedule.daily_leader == current_user || schedule.reading_event.current_daily_leader?(current_user, schedule)
-
return true
-
end
-
-
# 管理员可以查看
-
current_user.can_approve_events?
-
end
-
-
def check_in_params
-
params.require(:check_in).permit(:content, :word_count, :status)
-
end
-
-
def check_in_response_data(check_in, detailed: false, format_content: false)
-
base_data = {
-
id: check_in.id,
-
user: user_info(check_in.user),
-
reading_schedule: {
-
id: check_in.reading_schedule.id,
-
day_number: check_in.reading_schedule.day_number,
-
date: check_in.reading_schedule.date,
-
reading_progress: check_in.reading_schedule.reading_progress
-
},
-
content: check_in.content,
-
formatted_content: format_content ? check_in.formatted_content : nil,
-
content_preview: check_in.content_preview(150),
-
word_count: check_in.word_count,
-
status: check_in.status,
-
submitted_at: check_in.submitted_at,
-
updated_at: check_in.updated_at,
-
flowers_count: check_in.flowers_count,
-
engagement_score: check_in.engagement_score,
-
quality_score: check_in.quality_score,
-
keywords: check_in.keywords(5),
-
reading_time: check_in.reading_time_estimate,
-
can_be_edited: check_in.can_be_edited?,
-
can_receive_flowers: check_in.can_receive_flowers?,
-
high_quality: check_in.high_quality?,
-
has_formatting_issues: check_in.has_formatting_issues?,
-
contains_sensitive_words: check_in.contains_sensitive_words?
-
}
-
-
if detailed
-
base_data[:flowers] = check_in.flowers.map { |flower| flower_response_data(flower) }
-
base_data[:comments_count] = check_in.comments.count
-
base_data[:reading_event] = {
-
id: check_in.reading_event&.id,
-
title: check_in.reading_event&.title
-
}
-
base_data[:enrollment] = {
-
id: check_in.enrollment&.id,
-
completion_rate: check_in.enrollment&.completion_rate
-
}
-
base_data[:content_summary] = check_in.content_summary(200)
-
base_data[:compliance_check] = check_in.compliance_check
-
end
-
-
base_data
-
end
-
-
def flower_response_data(flower)
-
{
-
id: flower.id,
-
giver: user_info(flower.giver),
-
comment: flower.comment,
-
amount: flower.amount,
-
created_at: flower.created_at
-
}
-
end
-
-
def user_info(user)
-
return nil unless user
-
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
}
-
end
-
-
def schedule_basic_info(schedule)
-
{
-
id: schedule.id,
-
day_number: schedule.day_number,
-
date: schedule.date,
-
reading_progress: schedule.reading_progress,
-
daily_leader: schedule.daily_leader ? user_info(schedule.daily_leader) : nil
-
}
-
end
-
-
def schedule_check_in_statistics(schedule)
-
check_ins = schedule.check_ins
-
total = check_ins.count
-
today = check_ins.today.count
-
-
{
-
total: total,
-
today: today,
-
normal: check_ins.normal.count,
-
supplement: check_ins.supplement.count,
-
late: check_ins.late.count,
-
total_words: check_ins.sum(:word_count),
-
average_words: total > 0 ? (check_ins.sum(:word_count).to_f / total).round(2) : 0,
-
flowers_given: check_ins.joins(:flowers).count
-
}
-
end
-
-
def user_check_in_statistics(user)
-
check_ins = user.check_ins.includes(:reading_schedule)
-
-
total_check_ins = check_ins.count
-
this_month = check_ins.joins(:reading_schedule)
-
.where('reading_schedules.date >= ?', Date.current.beginning_of_month)
-
.count
-
-
{
-
total_check_ins: total_check_ins,
-
this_month: this_month,
-
current_streak: calculate_current_streak(user),
-
longest_streak: calculate_longest_streak(user),
-
total_words: check_ins.sum(:word_count),
-
average_words: total_check_ins > 0 ? (check_ins.sum(:word_count).to_f / total_check_ins).round(2) : 0,
-
flowers_received: user.flowers_received_count || 0,
-
engagement_score: user.check_ins.average(:engagement_score)&.round(2) || 0
-
}
-
end
-
-
def calculate_current_streak(user)
-
# 计算当前连续打卡天数
-
streak = 0
-
date = Date.current
-
-
while date >= Date.current - 30.days # 最多计算30天
-
if user.check_ins.joins(:reading_schedule).where('reading_schedules.date = ?', date).exists?
-
streak += 1
-
date -= 1.day
-
else
-
break
-
end
-
end
-
-
streak
-
end
-
-
def calculate_longest_streak(user)
-
# 计算历史最长连续打卡天数
-
# 这里可以优化为更高效的算法
-
check_in_dates = user.check_ins.joins(:reading_schedule)
-
.pluck('reading_schedules.date')
-
.sort.uniq
-
-
return 0 if check_in_dates.empty?
-
-
longest_streak = 1
-
current_streak = 1
-
-
check_in_dates.each_cons do |date1, date2|
-
if date2 == date1 + 1.day
-
current_streak += 1
-
longest_streak = [longest_streak, current_streak].max
-
else
-
current_streak = 1
-
end
-
end
-
-
longest_streak
-
end
-
-
# 辅助方法
-
def safe_integer_param(param)
-
return nil if param.blank?
-
-
Integer(param)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
-
def parse_date_param(param)
-
return nil if param.blank?
-
-
Date.parse(param.to_s)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
class Api::V1::ContentExportController < Api::V1::BaseController
-
before_action :authenticate_user!
-
-
# GET /api/v1/content_export/statistics
-
# 导出统计信息
-
def statistics
-
export_params = build_export_params
-
-
stats = ContentExportService.export_statistics(export_params)
-
-
render_success(
-
data: stats,
-
message: '导出统计信息获取成功'
-
)
-
rescue => e
-
render_error(
-
message: '获取导出统计信息失败',
-
errors: [e.message],
-
code: 'EXPORT_STATISTICS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_export/preview
-
# 导出预览
-
def preview
-
export_params = build_export_params
-
-
# 限制预览数量
-
export_params[:limit] = 5
-
-
check_ins = get_check_ins_for_preview(export_params)
-
-
render_success(
-
data: {
-
check_ins: check_ins.map(&:to_search_result_h),
-
total_count: estimate_total_count(export_params),
-
preview: true,
-
limit: 5
-
},
-
message: '导出预览生成成功'
-
)
-
rescue => e
-
render_error(
-
message: '生成导出预览失败',
-
errors: [e.message],
-
code: 'EXPORT_PREVIEW_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_export/export
-
# 执行导出
-
def export
-
export_params = build_export_params
-
-
# 验证导出权限
-
unless can_export_content?(export_params)
-
render_error(
-
message: '权限不足,无法导出这些内容',
-
code: 'EXPORT_PERMISSION_DENIED',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 执行导出
-
result = ContentExportService.export(export_params)
-
-
if result.success?
-
# 记录导出操作
-
log_export_operation(export_params, result)
-
-
send_data result.content,
-
filename: result.filename,
-
type: result.content_type,
-
disposition: 'attachment'
-
else
-
render_error(
-
message: '导出失败',
-
errors: [result.content],
-
code: 'EXPORT_FAILED'
-
)
-
end
-
rescue => e
-
render_error(
-
message: '导出过程中发生错误',
-
errors: [e.message],
-
code: 'EXPORT_ERROR'
-
)
-
end
-
-
# POST /api/v1/content_export/batch_export
-
# 批量导出
-
def batch_export
-
export_requests = params[:export_requests]
-
-
unless export_requests.is_a?(Array) && export_requests.any?
-
render_error(
-
message: '请提供导出请求列表',
-
code: 'INVALID_EXPORT_REQUESTS',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 验证批量导出权限
-
unless current_user.can_approve_events? # 只有管理员可以批量导出
-
render_error(
-
message: '权限不足,只有管理员可以批量导出',
-
code: 'BATCH_EXPORT_PERMISSION_DENIED',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 执行批量导出
-
results = ContentExportService.batch_export(export_requests)
-
-
# 记录批量导出操作
-
log_batch_export_operation(export_requests, results)
-
-
render_success(
-
data: {
-
results: results.map { |result| export_result_to_h(result) },
-
total_requests: export_requests.count,
-
successful_exports: results.count(&:success?),
-
failed_exports: results.count { |r| !r.success? }
-
},
-
message: '批量导出完成'
-
)
-
rescue => e
-
render_error(
-
message: '批量导出过程中发生错误',
-
errors: [e.message],
-
code: 'BATCH_EXPORT_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_export/templates
-
# 获取导出模板
-
def templates
-
templates = [
-
{
-
id: 'personal',
-
name: '个人打卡记录',
-
description: '导出当前用户的所有打卡记录',
-
params: {
-
format: 'pdf',
-
include_metadata: true,
-
include_comments: true,
-
include_flowers: true,
-
sort_by: 'created_at',
-
sort_direction: 'desc'
-
}
-
},
-
{
-
id: 'event_summary',
-
name: '活动汇总报告',
-
description: '导出指定活动的所有打卡记录汇总',
-
params: {
-
format: 'pdf',
-
include_metadata: true,
-
include_comments: false,
-
include_flowers: true,
-
sort_by: 'created_at',
-
sort_direction: 'asc'
-
}
-
},
-
{
-
id: 'quality_content',
-
name: '高质量内容精选',
-
description: '导出所有高质量打卡内容',
-
params: {
-
format: 'markdown',
-
include_metadata: true,
-
include_comments: true,
-
include_flowers: true,
-
sort_by: 'quality_score',
-
sort_direction: 'desc'
-
}
-
},
-
{
-
id: 'data_analysis',
-
name: '数据分析报告',
-
description: '导出用于数据分析的CSV格式数据',
-
params: {
-
format: 'csv',
-
include_metadata: false,
-
include_comments: false,
-
include_flowers: true,
-
sort_by: 'created_at',
-
sort_direction: 'asc'
-
}
-
}
-
]
-
-
render_success(
-
data: templates,
-
message: '导出模板获取成功'
-
)
-
end
-
-
# POST /api/v1/content_export/save_template
-
# 保存自定义模板
-
def save_template
-
template_name = params[:name]
-
template_params = params[:template]
-
-
if template_name.blank? || template_params.blank?
-
render_error(
-
message: '模板名称和参数不能为空',
-
code: 'INVALID_TEMPLATE',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 这里可以实现保存模板到数据库的逻辑
-
# 暂时返回成功响应
-
-
render_success(
-
message: '模板保存成功'
-
)
-
log_api_call('content_export#save_template')
-
rescue => e
-
render_error(
-
message: '保存模板失败',
-
errors: [e.message],
-
code: 'SAVE_TEMPLATE_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_export/history
-
# 导出历史
-
def export_history
-
limit = safe_integer_param(params[:limit]) || 20
-
-
# 这里可以实现获取用户导出历史的逻辑
-
# 暂时返回空数组
-
history_items = []
-
-
render_success(
-
data: {
-
history: history_items,
-
limit: limit
-
},
-
message: '导出历史获取成功'
-
)
-
rescue => e
-
render_error(
-
message: '获取导出历史失败',
-
errors: [e.message],
-
code: 'EXPORT_HISTORY_ERROR'
-
)
-
end
-
-
# POST /api/v1/content_export/schedule
-
# 定时导出
-
def schedule_export
-
export_params = build_export_params
-
schedule_time = params[:schedule_time]
-
schedule_type = params[:schedule_type] || 'once' # once, daily, weekly, monthly
-
-
unless schedule_time.present?
-
render_error(
-
message: '请提供导出时间',
-
code: 'SCHEDULE_TIME_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 这里可以实现定时导出的逻辑
-
# 暂时返回成功响应
-
-
render_success(
-
message: '定时导出设置成功'
-
)
-
log_api_call('content_export#schedule_export')
-
rescue => e
-
render_error(
-
message: '设置定时导出失败',
-
errors: [e.message],
-
code: 'SCHEDULE_EXPORT_ERROR'
-
)
-
end
-
-
private
-
-
# 构建导出参数
-
def build_export_params
-
permitted_params = params.permit(
-
:format, :check_in_ids, :user_id, :event_id, :date_from, :date_to,
-
:include_metadata, :include_comments, :include_flowers,
-
:sort_by, :sort_direction, :template, :limit
-
).to_h
-
-
# 设置默认值
-
permitted_params[:format] ||= 'pdf'
-
permitted_params[:include_metadata] = true if permitted_params[:include_metadata].nil?
-
permitted_params[:sort_by] ||= 'created_at'
-
permitted_params[:sort_direction] ||= 'desc'
-
-
permitted_params
-
end
-
-
# 检查导出权限
-
def can_export_content?(export_params)
-
# 用户可以导出自己的内容
-
return true if export_params[:user_id].blank? || export_params[:user_id] == current_user.id
-
-
# 管理员可以导出任何内容
-
return true if current_user.can_approve_events?
-
-
# 活动领读人可以导出自己活动的内容
-
if export_params[:event_id].present?
-
event = ReadingEvent.find_by(id: export_params[:event_id])
-
return true if event&.leader == current_user
-
end
-
-
false
-
end
-
-
# 获取预览用的打卡记录
-
def get_check_ins_for_preview(export_params)
-
query = CheckIn.includes(:user, :reading_schedule, :reading_event)
-
-
# 应用筛选条件(简化版)
-
if export_params[:user_id].present?
-
query = query.where(user_id: export_params[:user_id])
-
end
-
-
if export_params[:event_id].present?
-
query = query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: export_params[:event_id] })
-
end
-
-
if export_params[:date_from].present?
-
query = query.where('check_ins.created_at >= ?', export_params[:date_from].beginning_of_day)
-
end
-
-
if export_params[:date_to].present?
-
query = query.where('check_ins.created_at <= ?', export_params[:date_to].end_of_day)
-
end
-
-
# 限制数量并排序
-
query.limit(export_params[:limit] || 5).order(created_at: :desc)
-
end
-
-
# 估算总数量
-
def estimate_total_count(export_params)
-
query = CheckIn.all
-
-
# 应用相同的筛选条件
-
if export_params[:user_id].present?
-
query = query.where(user_id: export_params[:user_id])
-
end
-
-
if export_params[:event_id].present?
-
query = query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: export_params[:event_id] })
-
end
-
-
if export_params[:date_from].present?
-
query = query.where('check_ins.created_at >= ?', export_params[:date_from].beginning_of_day)
-
end
-
-
if export_params[:date_to].present?
-
query = query.where('check_ins.created_at <= ?', export_params[:date_to].end_of_day)
-
end
-
-
query.count
-
end
-
-
# 记录导出操作
-
def log_export_operation(export_params, result)
-
# 这里可以实现导出操作的记录逻辑
-
# 例如:保存到数据库、发送通知等
-
log_api_call('content_export#export', {
-
format: export_params[:format],
-
check_ins_count: result.check_ins_count,
-
filename: result.filename
-
})
-
end
-
-
# 记录批量导出操作
-
def log_batch_export_operation(export_requests, results)
-
log_api_call('content_export#batch_export', {
-
total_requests: export_requests.count,
-
successful_exports: results.count(&:success?),
-
failed_exports: results.count { |r| !r.success? }
-
})
-
end
-
-
# 转换导出结果为哈希
-
def export_result_to_h(result)
-
{
-
filename: result.filename,
-
content_type: result.content_type,
-
size: result.size,
-
check_ins_count: result.check_ins_count,
-
success: result.success?
-
}
-
end
-
-
# 辅助方法
-
def safe_integer_param(param)
-
return nil if param.blank?
-
Integer(param)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
class Api::V1::ContentReportsController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :set_check_in, only: [:create]
-
before_action :set_report, only: [:show, :update]
-
before_action :check_admin_permissions, only: [:index, :update, :batch_process, :statistics, :export]
-
-
# POST /api/v1/content_reports
-
# 创建举报
-
def create
-
reason = params[:reason]
-
description = params[:description]
-
-
if reason.blank?
-
render_error(
-
message: '请选择举报原因',
-
code: 'MISSING_REASON',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 验证举报原因
-
unless ContentReport.reasons.key?(reason.to_sym)
-
render_error(
-
message: '无效的举报原因',
-
code: 'INVALID_REASON',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 创建举报
-
result = ContentModerationService.create_report(
-
current_user,
-
@check_in,
-
reason: reason.to_sym,
-
description: description
-
)
-
-
if result[:success]
-
render_success(
-
data: content_report_response_data(result[:report]),
-
message: result[:message]
-
)
-
log_api_call('content_reports#create')
-
else
-
render_error(
-
message: result[:error],
-
errors: result[:errors],
-
code: 'REPORT_CREATE_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '提交举报失败',
-
errors: [e.message],
-
code: 'REPORT_CREATE_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_reports
-
# 获取举报列表(管理员)
-
def index
-
page = safe_integer_param(params[:page]) || 1
-
per_page = safe_integer_param(params[:per_page]) || 20
-
-
# 筛选参数
-
status = params[:status]
-
reason = params[:reason]
-
user_id = safe_integer_param(params[:user_id])
-
check_in_id = safe_integer_param(params[:check_in_id])
-
-
reports = ContentReport.includes(:user, :check_in, :admin)
-
.order(created_at: :desc)
-
-
# 应用筛选
-
reports = reports.where(status: status) if status.present?
-
reports = reports.where(reason: reason) if reason.present?
-
reports = reports.where(user_id: user_id) if user_id.present?
-
reports = reports.where(check_in_id: check_in_id) if check_in_id.present?
-
-
# 分页
-
paginated_reports = reports.page(page).per(per_page)
-
-
render_success(
-
data: {
-
reports: paginated_reports.map { |report| content_report_response_data(report, detailed: true) },
-
pagination: {
-
current_page: page,
-
per_page: per_page,
-
total_pages: paginated_reports.total_pages,
-
total_count: paginated_reports.total_count
-
},
-
filters: {
-
status: status,
-
reason: reason,
-
user_id: user_id,
-
check_in_id: check_in_id
-
}
-
},
-
message: '举报列表获取成功'
-
)
-
log_api_call('content_reports#index')
-
rescue => e
-
render_error(
-
message: '获取举报列表失败',
-
errors: [e.message],
-
code: 'REPORTS_LIST_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_reports/:id
-
# 获取举报详情
-
def show
-
unless @report.user == current_user || current_user.can_approve_events?
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
render_success(
-
data: content_report_response_data(@report, detailed: true),
-
message: '举报详情获取成功'
-
)
-
rescue => e
-
render_error(
-
message: '获取举报详情失败',
-
errors: [e.message],
-
code: 'REPORT_SHOW_ERROR'
-
)
-
end
-
-
# PUT /api/v1/content_reports/:id
-
# 处理举报(管理员)
-
def update
-
action = params[:action]
-
notes = params[:notes]
-
-
if action.blank?
-
render_error(
-
message: '请选择处理动作',
-
code: 'MISSING_ACTION',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 验证处理动作
-
valid_actions = %w[reviewed dismissed action_taken]
-
unless valid_actions.include?(action)
-
render_error(
-
message: '无效的处理动作',
-
code: 'INVALID_ACTION',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 处理举报
-
result = @report.review!(
-
admin: current_user,
-
notes: notes,
-
action: action.to_sym
-
)
-
-
if result
-
render_success(
-
data: content_report_response_data(@report, detailed: true),
-
message: '举报处理成功'
-
)
-
log_api_call('content_reports#update')
-
else
-
render_error(
-
message: '举报处理失败',
-
code: 'REPORT_UPDATE_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '处理举报失败',
-
errors: [e.message],
-
code: 'REPORT_UPDATE_ERROR'
-
)
-
end
-
-
# POST /api/v1/content_reports/batch_process
-
# 批量处理举报(管理员)
-
def batch_process
-
report_ids = params[:report_ids]
-
action = params[:action]
-
notes = params[:notes]
-
-
if report_ids.blank? || !report_ids.is_a?(Array)
-
render_error(
-
message: '请提供举报ID列表',
-
code: 'MISSING_REPORT_IDS',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
if action.blank?
-
render_error(
-
message: '请选择处理动作',
-
code: 'MISSING_ACTION',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 验证处理动作
-
valid_actions = %w[reviewed dismissed action_taken]
-
unless valid_actions.include?(action)
-
render_error(
-
message: '无效的处理动作',
-
code: 'INVALID_ACTION',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 批量处理
-
result = ContentModerationService.batch_process_reports(
-
current_user,
-
report_ids,
-
action: action.to_sym,
-
notes: notes
-
)
-
-
if result[:success]
-
render_success(
-
data: result,
-
message: "批量处理完成:成功处理 #{result[:processed_count]}/#{result[:total_count]} 个举报"
-
)
-
log_api_call('content_reports#batch_process')
-
else
-
render_error(
-
message: result[:error],
-
code: 'BATCH_PROCESS_FAILED'
-
)
-
end
-
rescue => e
-
render_error(
-
message: '批量处理失败',
-
errors: [e.message],
-
code: 'BATCH_PROCESS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_reports/statistics
-
# 获取举报统计(管理员)
-
def statistics
-
days = safe_integer_param(params[:days]) || 30
-
-
stats = ContentModerationService.get_statistics(days)
-
-
render_success(
-
data: stats,
-
message: '举报统计获取成功'
-
)
-
log_api_call('content_reports#statistics')
-
rescue => e
-
render_error(
-
message: '获取举报统计失败',
-
errors: [e.message],
-
code: 'STATISTICS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_reports/pending
-
# 获取待处理举报(管理员)
-
def pending
-
limit = safe_integer_param(params[:limit]) || 50
-
-
reports = ContentModerationService.get_pending_reports(limit: limit)
-
-
render_success(
-
data: {
-
reports: reports.map { |report| content_report_response_data(report, detailed: true) },
-
count: reports.count,
-
limit: limit
-
},
-
message: '待处理举报列表获取成功'
-
)
-
log_api_call('content_reports#pending')
-
rescue => e
-
render_error(
-
message: '获取待处理举报失败',
-
errors: [e.message],
-
code: 'PENDING_REPORTS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_reports/high_priority
-
# 获取高优先级举报(管理员)
-
def high_priority
-
reports = ContentModerationService.get_high_priority_reports
-
-
render_success(
-
data: {
-
reports: reports.map { |report| content_report_response_data(report, detailed: true) },
-
count: reports.count
-
},
-
message: '高优先级举报列表获取成功'
-
)
-
log_api_call('content_reports#high_priority')
-
rescue => e
-
render_error(
-
message: '获取高优先级举报失败',
-
errors: [e.message],
-
code: 'HIGH_PRIORITY_REPORTS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_reports/export
-
# 导出举报数据(管理员)
-
def export
-
start_date = parse_date_param(params[:start_date]) || 30.days.ago.to_date
-
end_date = parse_date_param(params[:end_date]) || Date.current
-
-
report = ContentModerationService.generate_moderation_report(start_date, end_date)
-
-
render_success(
-
data: report,
-
message: '举报报告生成成功'
-
)
-
log_api_call('content_reports#export')
-
rescue => e
-
render_error(
-
message: '导出举报数据失败',
-
errors: [e.message],
-
code: 'EXPORT_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_reports/my_reports
-
# 获取我的举报历史
-
def my_reports
-
page = safe_integer_param(params[:page]) || 1
-
per_page = safe_integer_param(params[:per_page]) || 20
-
-
reports = current_user.content_reports
-
.includes(:check_in, :admin)
-
.order(created_at: :desc)
-
.page(page)
-
.per(per_page)
-
-
render_success(
-
data: {
-
reports: reports.map { |report| content_report_response_data(report, detailed: true) },
-
pagination: {
-
current_page: page,
-
per_page: per_page,
-
total_pages: reports.total_pages,
-
total_count: reports.total_count
-
}
-
},
-
message: '我的举报历史获取成功'
-
)
-
log_api_call('content_reports#my_reports')
-
rescue => e
-
render_error(
-
message: '获取举报历史失败',
-
errors: [e.message],
-
code: 'MY_REPORTS_ERROR'
-
)
-
end
-
-
private
-
-
def set_check_in
-
check_in_id = safe_integer_param(params[:check_in_id])
-
-
unless check_in_id
-
render_error(
-
message: '打卡ID不能为空',
-
code: 'MISSING_CHECK_IN_ID',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
@check_in = CheckIn.find_by(id: check_in_id)
-
-
unless @check_in
-
render_error(
-
message: '打卡不存在',
-
code: 'CHECK_IN_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '打卡不存在',
-
code: 'CHECK_IN_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def set_report
-
@report = ContentReport.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '举报记录不存在',
-
code: 'REPORT_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def check_admin_permissions
-
unless current_user.can_approve_events?
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
end
-
end
-
-
def content_report_response_data(report, detailed: false)
-
base_data = {
-
id: report.id,
-
reason: report.reason,
-
reason_text: report.reason_text,
-
description: report.description,
-
status: report.status,
-
status_text: report.status_text,
-
created_at: report.created_at,
-
updated_at: report.updated_at
-
}
-
-
if detailed
-
base_data[:user] = {
-
id: report.user.id,
-
nickname: report.user.nickname,
-
avatar_url: report.user.avatar_url
-
}
-
base_data[:check_in] = {
-
id: report.check_in.id,
-
content: report.check_in.content_preview(200),
-
created_at: report.check_in.created_at,
-
user: {
-
id: report.check_in.user.id,
-
nickname: report.check_in.user.nickname
-
}
-
}
-
base_data[:admin] = report.admin ? {
-
id: report.admin.id,
-
nickname: report.admin.nickname
-
} : nil
-
base_data[:admin_notes] = report.admin_notes
-
base_data[:reviewed_at] = report.reviewed_at
-
end
-
-
base_data
-
end
-
-
# 辅助方法
-
def safe_integer_param(param)
-
return nil if param.blank?
-
Integer(param)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
-
def parse_date_param(param)
-
return nil if param.blank?
-
Date.parse(param.to_s)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
class Api::V1::ContentSearchController < Api::V1::BaseController
-
before_action :authenticate_user!
-
-
# GET /api/v1/content_search
-
# 内容搜索
-
def index
-
search_params = build_search_params
-
-
# 执行搜索
-
result = ContentSearchService.search(search_params)
-
-
render_success(
-
data: result.to_h,
-
message: '搜索完成'
-
)
-
log_api_call('content_search#index')
-
rescue => e
-
render_error(
-
message: '搜索失败',
-
errors: [e.message],
-
code: 'SEARCH_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_search/advanced
-
# 高级搜索
-
def advanced
-
search_params = build_search_params
-
-
result = ContentSearchService.advanced_search(search_params)
-
-
render_success(
-
data: {
-
check_ins: result[:check_ins].map(&:to_search_result_h),
-
options: result[:options]
-
},
-
message: '高级搜索完成'
-
)
-
log_api_call('content_search#advanced')
-
rescue => e
-
render_error(
-
message: '高级搜索失败',
-
errors: [e.message],
-
code: 'ADVANCED_SEARCH_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_search/suggestions
-
# 搜索建议
-
def suggestions
-
query = params[:q]&.strip
-
-
if query.blank?
-
render_error(
-
message: '请输入搜索关键词',
-
code: 'EMPTY_QUERY',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
suggestions = generate_search_suggestions(query)
-
-
render_success(
-
data: {
-
query: query,
-
suggestions: suggestions
-
},
-
message: '搜索建议生成成功'
-
)
-
rescue => e
-
render_error(
-
message: '生成搜索建议失败',
-
errors: [e.message],
-
code: 'SUGGESTIONS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_search/popular_keywords
-
# 热门关键词
-
def popular_keywords
-
days = safe_integer_param(params[:days]) || 30
-
limit = safe_integer_param(params[:limit]) || 20
-
-
keywords = ContentSearchService.popular_keywords(limit, days)
-
-
render_success(
-
data: {
-
keywords: keywords,
-
period: "#{days}天",
-
updated_at: Time.current
-
},
-
message: '热门关键词获取成功'
-
)
-
rescue => e
-
render_error(
-
message: '获取热门关键词失败',
-
errors: [e.message],
-
code: 'POPULAR_KEYWORDS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_search/trends
-
# 搜索趋势
-
def trends
-
days = safe_integer_param(params[:days]) || 7
-
-
trends = ContentSearchService.search_trends(days)
-
-
render_success(
-
data: {
-
trends: trends,
-
period: "#{days}天"
-
},
-
message: '搜索趋势获取成功'
-
)
-
rescue => e
-
render_error(
-
message: '获取搜索趋势失败',
-
errors: [e.message],
-
code: 'TRENDS_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_search/related/:check_in_id
-
# 相关内容推荐
-
def related
-
check_in_id = safe_integer_param(params[:check_in_id])
-
-
unless check_in_id
-
render_error(
-
message: '打卡ID不能为空',
-
code: 'INVALID_CHECK_IN_ID',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
check_in = CheckIn.find_by(id: check_in_id)
-
-
unless check_in
-
render_error(
-
message: '打卡记录不存在',
-
code: 'CHECK_IN_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限(只有活动参与者可以查看相关内容)
-
unless current_user.enrolled?(check_in.reading_event) ||
-
check_in.reading_event.leader == current_user ||
-
current_user.can_approve_events?
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
limit = safe_integer_param(params[:limit]) || 5
-
related_check_ins = ContentSearchService.recommend_related(check_in, limit)
-
-
render_success(
-
data: {
-
original_check_in: check_in.to_search_result_h,
-
related_check_ins: related_check_ins.map(&:to_search_result_h),
-
limit: limit
-
},
-
message: '相关内容推荐成功'
-
)
-
log_api_call('content_search#related')
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '打卡记录不存在',
-
code: 'CHECK_IN_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '获取相关内容失败',
-
errors: [e.message],
-
code: 'RELATED_CONTENT_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_search/facets
-
# 搜索统计
-
def facets
-
search_params = build_search_params.reject { |_, v| v.blank? }
-
-
if search_params.empty?
-
render_error(
-
message: '请提供搜索条件',
-
code: 'EMPTY_SEARCH_PARAMS',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
result = ContentSearchService.search(search_params)
-
-
render_success(
-
data: {
-
facets: result.facets,
-
total_count: result.total_count,
-
search_params: search_params
-
},
-
message: '搜索统计获取成功'
-
)
-
rescue => e
-
render_error(
-
message: '获取搜索统计失败',
-
errors: [e.message],
-
code: 'FACETS_ERROR'
-
)
-
end
-
-
# POST /api/v1/content_search/save_search
-
# 保存搜索历史
-
def save_search
-
query = params[:query]&.strip
-
search_type = params[:search_type] || 'basic'
-
-
if query.blank?
-
render_error(
-
message: '搜索内容不能为空',
-
code: 'EMPTY_QUERY',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 这里可以实现搜索历史保存逻辑
-
# 例如:保存到用户的搜索历史记录中
-
-
render_success(
-
message: '搜索历史保存成功'
-
)
-
log_api_call('content_search#save_search')
-
rescue => e
-
render_error(
-
message: '保存搜索历史失败',
-
errors: [e.message],
-
code: 'SAVE_SEARCH_ERROR'
-
)
-
end
-
-
# GET /api/v1/content_search/history
-
# 搜索历史
-
def history
-
limit = safe_integer_param(params[:limit]) || 10
-
-
# 这里可以实现获取用户搜索历史的逻辑
-
# 暂时返回空数组
-
history_items = []
-
-
render_success(
-
data: {
-
history: history_items,
-
limit: limit
-
},
-
message: '搜索历史获取成功'
-
)
-
rescue => e
-
render_error(
-
message: '获取搜索历史失败',
-
errors: [e.message],
-
code: 'SEARCH_HISTORY_ERROR'
-
)
-
end
-
-
private
-
-
# 构建搜索参数
-
def build_search_params
-
params.permit(
-
:query, :event_id, :user_id, :date_from, :date_to, :status,
-
:quality_min, :quality_max, :keywords, :sort_by, :sort_direction,
-
:page, :per_page
-
).to_h
-
end
-
-
# 生成搜索建议
-
def generate_search_suggestions(query)
-
suggestions = []
-
-
# 拼写检查建议
-
suggestions.concat(spell_check_suggestions(query))
-
-
# 热门关键词建议
-
popular_keywords = ContentSearchService.popular_keywords(10, 7)
-
matching_keywords = popular_keywords.keys.select { |keyword| keyword.include?(query) }
-
suggestions.concat(matching_keywords.map { |keyword| "#{keyword} (#{popular_keywords[keyword]}次)" })
-
-
# 相关搜索建议
-
suggestions.concat(related_search_suggestions(query))
-
-
suggestions.uniq.first(10)
-
end
-
-
# 拼写检查建议
-
def spell_check_suggestions(query)
-
# 简化的拼写检查实现
-
# 实际应用中可以使用更复杂的算法
-
-
suggestions = []
-
popular_keywords = ContentSearchService.popular_keywords(50, 30)
-
-
# 简单的编辑距离计算
-
popular_keywords.keys.each do |keyword|
-
distance = levenshtein_distance(query.downcase, keyword.downcase)
-
if distance <= 2 && distance > 0
-
suggestions << keyword
-
end
-
end
-
-
suggestions.first(5)
-
end
-
-
# 相关搜索建议
-
def related_search_suggestions(query)
-
# 基于用户历史和热门搜索生成相关建议
-
suggestions = []
-
-
# 可以添加更多相关搜索逻辑
-
suggestions
-
end
-
-
# 计算编辑距离(简化版)
-
def levenshtein_distance(str1, str2)
-
matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
-
-
(0..str1.length).each { |i| matrix[i][0] = i }
-
(0..str2.length).each { |j| matrix[0][j] = j }
-
-
(1..str1.length).each do |i|
-
(1..str2.length).each do |j|
-
cost = str1[i-1] == str2[j-1] ? 0 : 1
-
matrix[i][j] = [
-
matrix[i-1][j] + 1, # deletion
-
matrix[i][j-1] + 1, # insertion
-
matrix[i-1][j-1] + cost # substitution
-
].min
-
end
-
end
-
-
matrix[str1.length][str2.length]
-
end
-
-
# 辅助方法
-
def safe_integer_param(param)
-
return nil if param.blank?
-
Integer(param)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
class Api::V1::DailyLeadingsController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :set_reading_event
-
before_action :set_reading_schedule
-
before_action :set_daily_leading, only: [:show, :update, :destroy]
-
-
# POST /api/v1/reading_schedules/:reading_schedule_id/daily_leading
-
# 创建领读内容
-
def create
-
# 检查权限:领读人(权限窗口内)或活动创建者
-
unless can_create_daily_leading?
-
render_error(
-
message: '权限不足,只能在指定时间窗口内发布领读内容',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
return unless validate_required_fields(:content)
-
-
ActiveRecord::Base.transaction do
-
daily_leading = @reading_schedule.build_daily_leading(
-
reading_suggestion: params[:reading_suggestion] || params[:content],
-
questions: params[:questions] || "暂无问题",
-
leader: current_user
-
)
-
-
if daily_leading.save
-
# 通知领读内容已发布 (暂时注释掉,因为服务尚未实现)
-
# @reading_schedule.notify_leading_content_published
-
-
leading_data = build_daily_leading_data(daily_leading)
-
render_success(
-
data: leading_data,
-
message: '领读内容发布成功'
-
)
-
log_api_call('daily_leadings#create')
-
else
-
render_error(
-
message: '领读内容发布失败',
-
errors: daily_leading.errors.full_messages,
-
code: 'VALIDATION_ERROR'
-
)
-
end
-
end
-
rescue => e
-
render_error(
-
message: '领读内容发布失败',
-
errors: [e.message],
-
code: 'DAILY_LEADING_ERROR'
-
)
-
end
-
-
# GET /api/v1/reading_schedules/:reading_schedule_id/daily_leading
-
# 获取领读内容
-
def show
-
unless can_view_daily_leading?
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
if @daily_leading
-
leading_data = build_daily_leading_data(@daily_leading, detailed: true)
-
render_success(data: leading_data)
-
else
-
render_success(
-
data: nil,
-
message: '暂无领读内容'
-
)
-
end
-
-
log_api_call('daily_leadings#show')
-
end
-
-
# PUT/PATCH /api/v1/reading_schedules/:reading_schedule_id/daily_leading
-
# 更新领读内容
-
def update
-
unless @daily_leading
-
render_error(
-
message: '领读内容不存在',
-
code: 'DAILY_LEADING_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限:领读人(权限窗口内)或活动创建者
-
unless can_update_daily_leading?
-
render_error(
-
message: '权限不足,只能在指定时间窗口内更新领读内容',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
update_params = {}
-
if params[:content].present? || params[:reading_suggestion].present?
-
update_params[:reading_suggestion] = params[:reading_suggestion] || params[:content]
-
end
-
if params[:questions].present?
-
update_params[:questions] = params[:questions]
-
end
-
-
if update_params.empty?
-
render_error(
-
message: '没有可更新的字段',
-
code: 'NO_UPDATABLE_FIELDS'
-
)
-
return
-
end
-
-
ActiveRecord::Base.transaction do
-
if @daily_leading.update(update_params)
-
leading_data = build_daily_leading_data(@daily_leading, detailed: true)
-
render_success(
-
data: leading_data,
-
message: '领读内容更新成功'
-
)
-
log_api_call('daily_leadings#update')
-
else
-
render_error(
-
message: '领读内容更新失败',
-
errors: @daily_leading.errors.full_messages,
-
code: 'VALIDATION_ERROR'
-
)
-
end
-
end
-
rescue => e
-
render_error(
-
message: '领读内容更新失败',
-
errors: [e.message],
-
code: 'DAILY_LEADING_UPDATE_ERROR'
-
)
-
end
-
-
# DELETE /api/v1/reading_schedules/:reading_schedule_id/daily_leading
-
# 删除领读内容
-
def destroy
-
unless @daily_leading
-
render_error(
-
message: '领读内容不存在',
-
code: 'DAILY_LEADING_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限:只有活动创建者可以删除领读内容
-
unless @reading_event.leader == current_user
-
render_error(
-
message: '只有活动创建者可以删除领读内容',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
ActiveRecord::Base.transaction do
-
@daily_leading.destroy!
-
render_success(message: '领读内容删除成功')
-
log_api_call('daily_leadings#destroy')
-
end
-
rescue ActiveRecord::RecordNotDestroyed
-
render_error(
-
message: '领读内容删除失败',
-
code: 'DELETE_FAILED'
-
)
-
end
-
-
private
-
-
def set_reading_event
-
event_id = params[:reading_event_id]
-
@reading_event = ReadingEvent.find(event_id)
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def set_reading_schedule
-
schedule_id = params[:reading_schedule_id]
-
@reading_schedule = @reading_event.reading_schedules.find(schedule_id)
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def set_daily_leading
-
@daily_leading = @reading_schedule.daily_leading
-
rescue ActiveRecord::RecordNotFound
-
@daily_leading = nil
-
end
-
-
def build_daily_leading_data(daily_leading, detailed: false)
-
data = {
-
id: daily_leading.id,
-
reading_suggestion: daily_leading.reading_suggestion,
-
questions: daily_leading.questions,
-
created_at: daily_leading.created_at,
-
updated_at: daily_leading.updated_at
-
}
-
-
if detailed
-
data[:reading_schedule] = {
-
id: daily_leading.reading_schedule.id,
-
day_number: daily_leading.reading_schedule.day_number,
-
date: daily_leading.reading_schedule.date
-
}
-
-
data[:reading_event] = {
-
id: daily_leading.reading_schedule.reading_event.id,
-
title: daily_leading.reading_schedule.reading_event.title,
-
book_name: daily_leading.reading_schedule.reading_event.book_name
-
}
-
-
data[:leader] = daily_leading.leader ? {
-
id: daily_leading.leader.id,
-
nickname: daily_leading.leader.nickname
-
} : nil
-
-
data[:permissions] = {
-
can_view: can_view_daily_leading?,
-
can_update: can_update_daily_leading?,
-
can_delete: can_delete_daily_leading?
-
}
-
end
-
-
data
-
end
-
-
# 权限检查方法
-
def can_create_daily_leading?
-
# 活动创建者始终可以创建
-
return true if @reading_event.leader == current_user
-
-
# 领读人在权限窗口内可以创建
-
return true if @reading_schedule.daily_leader == current_user &&
-
@reading_schedule.can_publish_leading_content?
-
-
false
-
end
-
-
def can_view_daily_leading?
-
# 活动创建者、领读人、参与者都可以查看
-
return true if @reading_event.leader == current_user
-
return true if @reading_schedule.daily_leader == current_user
-
return true if @reading_event.participants.include?(current_user)
-
false
-
end
-
-
def can_update_daily_leading?
-
# 活动创建者始终可以更新
-
return true if @reading_event.leader == current_user
-
-
# 领读人在权限窗口内可以更新
-
return true if @reading_schedule.daily_leader == current_user &&
-
@reading_schedule.can_publish_leading_content?
-
-
false
-
end
-
-
def can_delete_daily_leading?
-
# 只有活动创建者可以删除
-
@reading_event.leader == current_user
-
end
-
end
-
class Api::V1::EventEnrollmentsController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :set_reading_event
-
before_action :set_enrollment, only: [:show, :cancel, :update]
-
-
# POST /api/v1/event_enrollments
-
# 报名参加活动
-
def create
-
return unless validate_required_fields(:reading_event_id)
-
-
ActiveRecord::Base.transaction do
-
# 检查活动是否可以报名
-
unless @reading_event.can_enroll?
-
render_error(
-
message: @reading_event.enrollment_error_message || '活动当前无法报名',
-
code: 'CANNOT_ENROLL',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 检查用户是否已经报名
-
existing_enrollment = @reading_event.event_enrollments.find_by(user: current_user)
-
if existing_enrollment
-
render_error(
-
message: '您已经报名过此活动',
-
code: 'ALREADY_ENROLLED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 创建报名记录
-
enrollment = @reading_event.event_enrollments.build(
-
user: current_user,
-
enrollment_type: params[:enrollment_type]&.to_s || 'participant',
-
status: 'enrolled',
-
enrollment_date: Time.current
-
)
-
-
# 处理费用(如果有)
-
if @reading_event.fee_type != 'free'
-
fee_amount = @reading_event.fee_amount
-
enrollment.fee_paid_amount = fee_amount
-
-
# 这里应该调用支付服务
-
# payment_result = PaymentService.process(current_user, fee_amount, @reading_event)
-
# unless payment_result.success?
-
# render_error(message: '支付失败', code: 'PAYMENT_FAILED', status: :unprocessable_entity)
-
# return
-
# end
-
end
-
-
if enrollment.save
-
# 发送报名确认通知(暂时注释掉,因为服务未实现)
-
# enrollment.notify_enrollment_confirmation
-
-
enrollment_data = build_enrollment_data(enrollment)
-
render_success(
-
data: enrollment_data,
-
message: '报名成功'
-
)
-
log_api_call('event_enrollments#create')
-
else
-
render_error(
-
message: '报名失败',
-
errors: enrollment.errors.full_messages,
-
code: 'VALIDATION_ERROR'
-
)
-
end
-
end
-
rescue => e
-
render_error(
-
message: '报名处理失败',
-
errors: [e.message],
-
code: 'ENROLLMENT_ERROR'
-
)
-
end
-
-
# GET /api/v1/event_enrollments/:id
-
# 获取报名详情
-
def show
-
unless @enrollment
-
render_error(
-
message: '报名记录不存在',
-
code: 'ENROLLMENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限:只有报名者本人或活动创建者可以查看
-
unless @enrollment.user == current_user || @reading_event.leader == current_user
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
enrollment_data = build_enrollment_data(@enrollment, detailed: true)
-
render_success(data: enrollment_data)
-
log_api_call('event_enrollments#show')
-
end
-
-
# POST /api/v1/event_enrollments/:id/cancel
-
# 取消报名
-
def cancel
-
unless @enrollment
-
render_error(
-
message: '报名记录不存在',
-
code: 'ENROLLMENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限:只有报名者本人可以取消
-
unless @enrollment.user == current_user
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 检查是否可以取消
-
unless @enrollment.can_cancel?
-
render_error(
-
message: @enrollment.cancellation_error_message || '当前状态无法取消报名',
-
code: 'CANNOT_CANCEL',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
ActiveRecord::Base.transaction do
-
# 处理退款(如果有)
-
if @enrollment.fee_paid_amount > 0
-
@enrollment.process_refund!
-
end
-
-
# 更新状态
-
@enrollment.update!(status: 'cancelled')
-
-
# 发送取消通知
-
# EnrollmentNotificationService.notify_cancellation(@enrollment)
-
-
render_success(
-
data: build_enrollment_data(@enrollment),
-
message: '报名已取消'
-
)
-
log_api_call('event_enrollments#cancel')
-
end
-
rescue => e
-
render_error(
-
message: '取消报名失败',
-
errors: [e.message],
-
code: 'CANCELLATION_ERROR'
-
)
-
end
-
-
# PUT/PATCH /api/v1/event_enrollments/:id
-
# 更新报名信息(仅限特定字段)
-
def update
-
unless @enrollment
-
render_error(
-
message: '报名记录不存在',
-
code: 'ENROLLMENT_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限:只有报名者本人可以更新
-
unless @enrollment.user == current_user
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 只允许更新特定字段
-
allowed_fields = [:enrollment_type]
-
update_params = params.slice(*allowed_fields).compact
-
-
if update_params.empty?
-
render_error(
-
message: '没有可更新的字段',
-
code: 'NO_UPDATABLE_FIELDS'
-
)
-
return
-
end
-
-
ActiveRecord::Base.transaction do
-
if @enrollment.update(update_params)
-
render_success(
-
data: build_enrollment_data(@enrollment),
-
message: '报名信息更新成功'
-
)
-
log_api_call('event_enrollments#update')
-
else
-
render_error(
-
message: '更新失败',
-
errors: @enrollment.errors.full_messages,
-
code: 'VALIDATION_ERROR'
-
)
-
end
-
end
-
rescue => e
-
render_error(
-
message: '更新失败',
-
errors: [e.message],
-
code: 'UPDATE_ERROR'
-
)
-
end
-
-
# GET /api/v1/reading_events/:reading_event_id/enrollments
-
# 获取活动的报名列表(活动创建者可用)
-
def index
-
# 检查权限:只有活动创建者可以查看报名列表
-
unless @reading_event.leader == current_user
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 获取报名列表
-
enrollments = @reading_event.event_enrollments
-
.includes(:user)
-
.order(enrollment_date: :desc)
-
-
# 分页
-
pagination = pagination_params
-
enrollments = enrollments.page(pagination[:page]).per(pagination[:per_page])
-
-
# 构建响应数据
-
enrollments_data = enrollments.map do |enrollment|
-
build_enrollment_data(enrollment, detailed: true)
-
end
-
-
render_success(
-
data: enrollments_data,
-
meta: pagination_meta(enrollments)
-
)
-
log_api_call('event_enrollments#index')
-
end
-
-
# GET /api/v1/reading_events/:reading_event_id/enrollments/statistics
-
# 获取活动报名统计(活动创建者可用)
-
def statistics
-
# 检查权限:只有活动创建者可以查看统计
-
unless @reading_event.leader == current_user
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 计算统计数据
-
stats = @reading_event.event_enrollments.calculate_enrollment_statistics
-
-
render_success(data: stats)
-
log_api_call('event_enrollments#statistics')
-
end
-
-
private
-
-
def set_reading_event
-
event_id = params[:reading_event_id] || params[:id]
-
@reading_event = ReadingEvent.find(event_id)
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def set_enrollment
-
@enrollment = @reading_event.event_enrollments.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
@enrollment = nil
-
end
-
-
def build_enrollment_data(enrollment, detailed: false)
-
data = {
-
id: enrollment.id,
-
enrollment_type: enrollment.enrollment_type,
-
status: enrollment.status,
-
enrollment_date: enrollment.enrollment_date,
-
completion_rate: enrollment.completion_rate,
-
check_ins_count: enrollment.check_ins_count,
-
leader_days_count: enrollment.leader_days_count,
-
flowers_received_count: enrollment.flowers_received_count,
-
fee_paid_amount: enrollment.fee_paid_amount,
-
fee_refund_amount: enrollment.fee_refund_amount,
-
refund_status: enrollment.refund_status,
-
created_at: enrollment.created_at,
-
updated_at: enrollment.updated_at
-
}
-
-
if detailed
-
data[:user] = {
-
id: enrollment.user.id,
-
nickname: enrollment.user.nickname,
-
avatar_url: enrollment.user.avatar_url
-
}
-
-
data[:reading_event] = {
-
id: enrollment.reading_event.id,
-
title: enrollment.reading_event.title,
-
book_name: enrollment.reading_event.book_name
-
}
-
-
data[:permissions] = {
-
can_cancel: enrollment.can_cancel?,
-
can_update: enrollment.user == current_user,
-
can_check_in: enrollment.can_check_in?,
-
can_receive_flowers: enrollment.can_receive_flowers?,
-
can_give_flowers: enrollment.can_give_flowers?
-
}
-
-
data[:status_info] = {
-
can_participate: enrollment.can_participate?,
-
is_completed: enrollment.is_completed?,
-
eligible_for_completion_certificate: enrollment.eligible_for_completion_certificate?,
-
eligible_for_flower_certificate: enrollment.eligible_for_flower_certificate?
-
}
-
end
-
-
data
-
end
-
end
-
# frozen_string_literal: true
-
-
class Api::V1::FlowerCommentsController < ApplicationController
-
before_action :authenticate_user!
-
before_action :set_flower
-
-
# POST /api/flowers/:flower_id/comments
-
def create
-
result = FlowerCommentService.add_comment_to_flower(@flower, current_user, comment_params[:content])
-
-
if result[:success]
-
render json: result, status: :created
-
else
-
render json: { error: result[:error] }, status: :unprocessable_entity
-
end
-
end
-
-
# GET /api/flowers/:flower_id/comments
-
def index
-
page = params[:page] || 1
-
limit = params[:limit] || 10
-
-
result = FlowerCommentService.get_flower_comments(@flower, page, limit, current_user)
-
-
render json: result
-
end
-
-
# GET /api/flowers/:flower_id/comments/stats
-
def stats
-
result = FlowerCommentService.get_flower_comment_stats(@flower)
-
-
render json: result
-
end
-
-
# DELETE /api/flowers/:flower_id/comments/:id
-
def destroy
-
comment = @flower.comments.find(params[:id])
-
result = FlowerCommentService.delete_flower_comment(@flower, comment, current_user)
-
-
if result[:success]
-
render json: { message: result[:message] }
-
else
-
render json: { error: result[:error] }, status: :forbidden
-
end
-
end
-
-
# DELETE /api/flowers/:flower_id/comments/batch
-
def batch_destroy
-
return render json: { error: '需要管理员权限' }, status: :forbidden unless current_user.any_admin?
-
-
comment_ids = params[:comment_ids] || []
-
result = FlowerCommentService.batch_delete_flower_comments(@flower, comment_ids, current_user)
-
-
render json: result
-
end
-
-
# GET /api/flowers/:flower_id/comments/search
-
def search
-
keyword = params[:q] || params[:keyword]
-
page = params[:page] || 1
-
limit = params[:limit] || 10
-
-
result = FlowerCommentService.search_flower_comments(@flower, keyword, page, limit, current_user)
-
-
render json: result
-
end
-
-
private
-
-
def set_flower
-
@flower = Flower.find(params[:flower_id])
-
rescue ActiveRecord::RecordNotFound
-
render json: { error: '小红花不存在' }, status: :not_found
-
end
-
-
def comment_params
-
params.require(:comment).permit(:content)
-
end
-
end
-
class Api::V1::FlowerIncentivesController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :find_reading_event
-
before_action :check_event_participation
-
-
# 获取用户在活动中的配额信息
-
def quota_info
-
quota_info = FlowerIncentiveService.get_user_quota_info(current_user, @reading_event)
-
-
if quota_info[:error]
-
render json: {
-
success: false,
-
error: quota_info[:error]
-
}, status: :unprocessable_entity
-
else
-
render json: {
-
success: true,
-
data: quota_info
-
}
-
end
-
end
-
-
# 赠送小红花(带配额检查和确认提示)
-
def give_flower
-
# 验证参数
-
recipient_id = params[:recipient_id]
-
check_in_id = params[:check_in_id]
-
amount = params[:amount]&.to_i || 1
-
comment = params[:comment]
-
flower_type = params[:flower_type] || 'regular'
-
is_anonymous = params[:is_anonymous] == true
-
-
# 验证必要参数
-
unless recipient_id && check_in_id
-
return render json: {
-
success: false,
-
error: '缺少必要参数:recipient_id 和 check_in_id'
-
}, status: :bad_request
-
end
-
-
# 查找接收者和打卡记录
-
recipient = User.find_by(id: recipient_id)
-
check_in = CheckIn.find_by(id: check_in_id)
-
-
unless recipient && check_in
-
return render json: {
-
success: false,
-
error: '接收者或打卡记录不存在'
-
}, status: :not_found
-
end
-
-
# 验证打卡记录是否属于当前活动
-
if check_in.reading_schedule&.reading_event_id != @reading_event.id
-
return render json: {
-
success: false,
-
error: '该打卡记录不属于当前活动'
-
}, status: :unprocessable_entity
-
end
-
-
# 检查是否是给自己赠送
-
if recipient.id == current_user.id
-
return render json: {
-
success: false,
-
error: '不能给自己赠送小红花'
-
}, status: :unprocessable_entity
-
end
-
-
# 检查配额
-
unless FlowerIncentiveService.can_give_flower?(current_user, @reading_event, amount)
-
return render json: {
-
success: false,
-
error: '小红花配额不足',
-
quota_info: FlowerIncentiveService.get_user_quota_info(current_user, @reading_event)
-
}, status: :unprocessable_entity
-
end
-
-
# 根据请求类型处理(确认或直接赠送)
-
if params[:confirm] == true
-
# 用户已确认,执行赠送
-
result = FlowerIncentiveService.give_flower_with_quota(
-
current_user,
-
recipient,
-
check_in,
-
amount: amount,
-
comment: comment,
-
flower_type: flower_type,
-
is_anonymous: is_anonymous
-
)
-
-
if result[:success]
-
render json: {
-
success: true,
-
message: '小红花赠送成功!',
-
data: {
-
flower: result[:flower].as_json_for_api,
-
remaining_quota: result[:remaining_quota],
-
warning: '赠送成功后无法撤回,请谨慎操作'
-
}
-
}
-
else
-
render json: {
-
success: false,
-
error: result[:error],
-
message: '小红花赠送失败,请重试'
-
}, status: :unprocessable_entity
-
end
-
else
-
# 需要用户确认
-
quota_info = FlowerIncentiveService.get_user_quota_info(current_user, @reading_event)
-
-
render json: {
-
success: true,
-
require_confirmation: true,
-
message: '即将赠送小红花,此操作不可撤回,请确认',
-
data: {
-
recipient: recipient.as_json_for_api,
-
check_in: {
-
id: check_in.id,
-
content: check_in.content.truncate(100),
-
user: check_in.user.as_json_for_api
-
},
-
amount: amount,
-
comment: comment,
-
flower_type: flower_type,
-
is_anonymous: is_anonymous,
-
remaining_quota: quota_info[:remaining_flowers],
-
warning: '赠送成功后无法撤回,请谨慎确认'
-
}
-
}
-
end
-
end
-
-
# 获取活动的前三名排行榜
-
def top_three
-
if @reading_event.status != 'completed'
-
return render json: {
-
success: false,
-
error: '活动尚未结束,排行榜暂未生成'
-
}, status: :unprocessable_entity
-
end
-
-
result = FlowerIncentiveService.get_event_top_three(@reading_event)
-
-
if result[:error]
-
render json: {
-
success: false,
-
error: result[:error]
-
}, status: :unprocessable_entity
-
else
-
render json: {
-
success: true,
-
data: result
-
}
-
end
-
end
-
-
# 获取用户的证书历史
-
def my_certificates
-
certificates = FlowerIncentiveService.get_user_certificates(current_user)
-
-
render json: {
-
success: true,
-
data: certificates
-
}
-
end
-
-
# 获取证书详情
-
def certificate_detail
-
certificate_id = params[:certificate_id]
-
-
unless certificate_id
-
return render json: {
-
success: false,
-
error: '缺少证书ID'
-
}, status: :bad_request
-
end
-
-
certificate = FlowerCertificate.find_by(certificate_id: certificate_id)
-
-
unless certificate
-
return render json: {
-
success: false,
-
error: '证书不存在'
-
}, status: :not_found
-
end
-
-
# 检查权限(只有证书所有者或活动参与者可以查看)
-
if certificate.user_id != current_user.id && !@reading_event.participants.include?(current_user)
-
return render json: {
-
success: false,
-
error: '没有权限查看该证书'
-
}, status: :forbidden
-
end
-
-
render json: {
-
success: true,
-
data: {
-
certificate: certificate.as_json_for_api,
-
event: certificate.reading_event.as_json_for_api,
-
user: certificate.user.as_json_for_api,
-
share_url: certificate.share_url,
-
certificate_image_url: certificate.certificate_image_path
-
}
-
}
-
end
-
-
# 生成活动结束证书(管理员权限)
-
def finalize_certificates
-
unless current_user.any_admin?
-
return render json: {
-
success: false,
-
error: '没有权限执行此操作'
-
}, status: :forbidden
-
end
-
-
if @reading_event.status != 'completed'
-
return render json: {
-
success: false,
-
error: '只有已结束的活动才能生成证书'
-
}, status: :unprocessable_entity
-
end
-
-
result = FlowerIncentiveService.finalize_event_flower_certificates(@reading_event)
-
-
if result[:success]
-
render json: {
-
success: true,
-
message: '活动证书生成成功!',
-
data: result
-
}
-
else
-
render json: {
-
success: false,
-
error: result[:error]
-
}, status: :unprocessable_entity
-
end
-
end
-
-
# 活动开始时初始化配额(管理员权限)
-
def initialize_quotas
-
unless current_user.any_admin?
-
return render json: {
-
success: false,
-
error: '没有权限执行此操作'
-
}, status: :forbidden
-
end
-
-
max_flowers = params[:max_flowers]&.to_i || 3
-
-
if FlowerIncentiveService.initialize_event_flower_quotas(@reading_event, max_flowers: max_flowers)
-
render json: {
-
success: true,
-
message: '活动小红花配额初始化成功',
-
data: {
-
event: @reading_event.as_json_for_api,
-
max_flowers: max_flowers,
-
participants_count: @reading_event.participants.count
-
}
-
}
-
else
-
render json: {
-
success: false,
-
error: '配额初始化失败,请重试'
-
}, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def find_reading_event
-
@reading_event = ReadingEvent.find(params[:reading_event_id])
-
rescue ActiveRecord::RecordNotFound
-
render json: {
-
success: false,
-
error: '活动不存在'
-
}, status: :not_found
-
end
-
-
def check_event_participation
-
unless @reading_event.participants.include?(current_user) || current_user.any_admin?
-
render json: {
-
success: false,
-
error: '您尚未参与此活动或没有权限访问'
-
}, status: :forbidden
-
end
-
end
-
end
-
class Api::V1::FlowerLeaderboardsController < Api::V1::BaseController
-
before_action :authenticate_user!
-
-
# GET /api/v1/flower_leaderboards
-
# 获取小红花排行榜
-
def index
-
type = params[:type] || 'received'
-
period = safe_integer_param(params[:period]) || 30
-
limit = safe_integer_param(params[:limit]) || 20
-
-
# 验证参数
-
valid_types = %w[received given popular_check_ins generous_givers]
-
unless valid_types.include?(type)
-
render_error(
-
message: '无效的排行榜类型',
-
code: 'INVALID_TYPE',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
if period < 1 || period > 365
-
render_error(
-
message: '统计时间范围必须在1-365天之间',
-
code: 'INVALID_PERIOD',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
if limit < 1 || limit > 100
-
render_error(
-
message: '显示数量必须在1-100之间',
-
code: 'INVALID_LIMIT',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 获取排行榜数据
-
leaderboard = FlowerStatisticsService.get_flower_leaderboard(type, period, limit)
-
-
# 格式化响应数据
-
formatted_leaderboard = case type.to_sym
-
when :received
-
format_user_leaderboard(leaderboard)
-
when :given
-
format_user_leaderboard(leaderboard)
-
when :popular_check_ins
-
format_check_in_leaderboard(leaderboard)
-
when :generous_givers
-
format_user_leaderboard(leaderboard)
-
else
-
[]
-
end
-
-
render_success(
-
data: {
-
leaderboard_type: type,
-
period: period,
-
limit: limit,
-
leaderboard: formatted_leaderboard
-
},
-
message: '排行榜获取成功'
-
)
-
log_api_call('flower_leaderboards#index')
-
rescue => e
-
render_error(
-
message: '获取排行榜失败',
-
errors: [e.message],
-
code: 'LEADERBOARD_ERROR'
-
)
-
end
-
-
# GET /api/v1/flower_leaderboards/trends
-
# 获取小红花趋势数据
-
def trends
-
days = safe_integer_param(params[:days]) || 30
-
-
if days < 1 || days > 90
-
render_error(
-
message: '统计时间范围必须在1-90天之间',
-
code: 'INVALID_PERIOD',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
trends = FlowerStatisticsService.get_flower_trends(days)
-
-
render_success(
-
data: {
-
period: "#{days}天",
-
trends: trends,
-
summary: calculate_trends_summary(trends)
-
},
-
message: '趋势数据获取成功'
-
)
-
log_api_call('flower_leaderboards#trends')
-
rescue => e
-
render_error(
-
message: '获取趋势数据失败',
-
errors: [e.message],
-
code: 'TRENDS_ERROR'
-
)
-
end
-
-
# GET /api/v1/flower_leaderboards/statistics
-
# 获取小红花统计
-
def statistics
-
days = safe_integer_param(params[:days]) || 30
-
type = params[:type] # 'user' 或 'event'
-
id = safe_integer_param(params[:id])
-
-
if days < 1 || days > 365
-
render_error(
-
message: '统计时间范围必须在1-365天之间',
-
code: 'INVALID_PERIOD',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
data = case type
-
when 'user'
-
get_user_statistics(id, days)
-
when 'event'
-
get_event_statistics(id, days)
-
when 'incentive'
-
FlowerStatisticsService.get_incentive_statistics(days)
-
else
-
FlowerStatisticsService.get_incentive_statistics(days)
-
end
-
-
if data.nil?
-
render_error(
-
message: '无效的统计类型或ID',
-
code: 'INVALID_TYPE_OR_ID',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
render_success(
-
data: data,
-
message: '统计数据获取成功'
-
)
-
log_api_call('flower_leaderboards#statistics')
-
rescue => e
-
render_error(
-
message: '获取统计数据失败',
-
errors: [e.message],
-
code: 'STATISTICS_ERROR'
-
)
-
end
-
-
# GET /api/v1/flower_leaderboards/suggestions
-
# 获取小红花发放建议
-
def suggestions
-
limit = safe_integer_param(params[:limit]) || 5
-
-
if limit < 1 || limit > 20
-
render_error(
-
message: '建议数量必须在1-20之间',
-
code: 'INVALID_LIMIT',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
suggestions = FlowerStatisticsService.get_flower_suggestions(current_user, limit)
-
-
formatted_suggestions = suggestions.map do |suggestion|
-
case suggestion[:type]
-
when :check_in
-
{
-
id: suggestion[:check_in].id,
-
type: 'check_in',
-
title: suggestion[:check_in].content_preview(100),
-
author: {
-
id: suggestion[:check_in].user.id,
-
nickname: suggestion[:check_in].user.nickname,
-
avatar_url: suggestion[:check_in].user.avatar_url
-
},
-
created_at: suggestion[:check_in].created_at,
-
flowers_count: suggestion[:check_in].flowers_count,
-
reason: suggestion[:reason],
-
priority: suggestion[:priority]
-
}
-
when :user
-
{
-
id: suggestion[:user].id,
-
type: 'user',
-
nickname: suggestion[:user].nickname,
-
avatar_url: suggestion[:user].avatar_url,
-
reason: suggestion[:reason],
-
priority: suggestion[:priority]
-
}
-
end
-
end
-
-
render_success(
-
data: {
-
suggestions: formatted_suggestions,
-
limit: limit,
-
user_id: current_user.id
-
},
-
message: '发放建议获取成功'
-
)
-
log_api_call('flower_leaderboards#suggestions')
-
rescue => e
-
render_error(
-
message: '获取发放建议失败',
-
errors: [e.message],
-
code: 'SUGGESTIONS_ERROR'
-
)
-
end
-
-
# GET /api/v1/flower_leaderboards/my_ranking
-
# 获取当前用户的排名
-
def my_ranking
-
period = safe_integer_param(params[:period]) || 30
-
type = params[:type] || 'received'
-
-
if period < 1 || period > 365
-
render_error(
-
message: '统计时间范围必须在1-365天之间',
-
code: 'INVALID_PERIOD',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
# 获取排行榜
-
leaderboard = FlowerStatisticsService.get_flower_leaderboard(type, period, 1000)
-
-
# 查找当前用户的排名
-
my_ranking = case type.to_sym
-
when :received
-
leaderboard.index { |user| user[:id] == current_user.id }
-
when :given
-
leaderboard.index { |user| user[:id] == current_user.id }
-
else
-
nil
-
end
-
-
my_stats = FlowerStatisticsService.get_user_flower_stats(current_user, period)
-
-
render_success(
-
data: {
-
period: period,
-
type: type,
-
my_ranking: my_ranking ? my_ranking + 1 : nil, # 排名从1开始
-
total_users: leaderboard.count,
-
my_stats: my_stats,
-
top_10: leaderboard.first(10).map { |user| user[:id] },
-
percentage: calculate_ranking_percentage(my_ranking, leaderboard.count)
-
},
-
message: '个人排名获取成功'
-
)
-
log_api_call('flower_leaderboards#my_ranking')
-
rescue => e
-
render_error(
-
message: '获取个人排名失败',
-
errors: [e.message],
-
code: 'MY_RANKING_ERROR'
-
)
-
end
-
-
private
-
-
def get_user_statistics(user_id, days)
-
user = user_id ? User.find_by(id: user_id) : current_user
-
return nil unless user
-
-
FlowerStatisticsService.get_user_flower_stats(user, days)
-
end
-
-
def get_event_statistics(event_id, days)
-
event = ReadingEvent.find_by(id: event_id)
-
return nil unless event
-
-
FlowerStatisticsService.get_event_flower_stats(event, days)
-
end
-
-
def format_user_leaderboard(leaderboard)
-
leaderboard.map do |user|
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url,
-
total_flowers: user.total_flowers,
-
rank: leaderboard.index(user) + 1
-
}
-
end
-
end
-
-
def format_check_in_leaderboard(leaderboard)
-
leaderboard.map do |check_in|
-
{
-
id: check_in.id,
-
content: check_in.content_preview(100),
-
author: {
-
id: check_in.user.id,
-
nickname: check_in.user.nickname,
-
avatar_url: check_in.user.avatar_url
-
},
-
created_at: check_in.created_at,
-
flowers_count: check_in.flower_count,
-
rank: leaderboard.index(check_in) + 1
-
}
-
end
-
end
-
-
def calculate_trends_summary(trends)
-
total_flowers = trends.values.sum { |day| day[:total] }
-
avg_flowers = trends.values.sum { |day| day[:total] }.to_f / [trends.count, 1].max
-
max_flowers = trends.values.map { |day| day[:total] }.max || 0
-
min_flowers = trends.values.map { |day| day[:total] }.min || 0
-
-
{
-
total_flowers: total_flowers,
-
avg_flowers: avg_flowers.round(2),
-
max_flowers: max_flowers,
-
min_flowers: min_flowers,
-
trend_days: trends.keys.count
-
}
-
end
-
-
def calculate_ranking_percentage(rank, total_users)
-
return 0 if rank.nil? || total_users == 0
-
((total_users - rank + 1).to_f / total_users * 100).round(2)
-
end
-
-
# 辅助方法
-
def safe_integer_param(param)
-
return nil if param.blank?
-
Integer(param)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
class Api::V1::LeaderAssignmentsController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :set_reading_event
-
before_action :check_event_permissions
-
-
# POST /api/v1/reading_events/:reading_event_id/leader_assignments/auto_assign
-
# 自动分配领读人
-
def auto_assign
-
assignment_type = params[:assignment_type]&.to_sym || @reading_event.leader_assignment_type.to_sym
-
-
unless [:random, :balanced, :rotation, :voluntary].include?(assignment_type)
-
render_error(
-
message: '不支持的分配方式',
-
code: 'UNSUPPORTED_ASSIGNMENT_TYPE',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
options = {}
-
options[:max_leadership_count] = params[:max_leadership_count] if params[:max_leadership_count].present?
-
options[:volunteer_assignments] = params[:volunteer_assignments] if params[:volunteer_assignments].present?
-
-
service = LeaderAssignmentService.auto_assign_leaders!(@reading_event, assignment_type: assignment_type, options: options)
-
-
if service.success?
-
render_success(
-
data: {
-
assignment_type: service.result[:assignment_type],
-
assigned_count: service.result[:assigned_count],
-
statistics: get_assignment_statistics
-
},
-
message: service.result[:message]
-
)
-
log_api_call('leader_assignments#auto_assign')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'AUTO_ASSIGN_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '自动分配失败',
-
errors: [e.message],
-
code: 'AUTO_ASSIGN_ERROR'
-
)
-
end
-
-
# POST /api/v1/reading_events/:reading_event_id/leader_assignments/:schedule_id/claim
-
# 自由报名领读
-
def claim_leadership
-
schedule = @reading_event.reading_schedules.find(params[:schedule_id])
-
-
service = LeaderAssignmentService.claim_leadership!(@reading_event, current_user, schedule)
-
-
if service.success?
-
render_success(
-
data: service.result[:schedule_data],
-
message: service.result[:message]
-
)
-
log_api_call('leader_assignments#claim_leadership')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'CLAIM_LEADERSHIP_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '报名领读失败',
-
errors: [e.message],
-
code: 'CLAIM_LEADERSHIP_ERROR'
-
)
-
end
-
-
# POST /api/v1/reading_events/:reading_event_id/leader_assignments/:schedule_id/reassign
-
# 重新分配领读人
-
def reassign_leader
-
schedule = @reading_event.reading_schedules.find(params[:schedule_id])
-
-
unless params[:new_leader_id].present?
-
render_error(
-
message: '请指定新的领读人',
-
code: 'NEW_LEADER_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
new_leader = User.find(params[:new_leader_id])
-
unless new_leader
-
render_error(
-
message: '新领读人不存在',
-
code: 'NEW_LEADER_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
service = LeaderAssignmentService.reassign_leader!(@reading_event, schedule, new_leader)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('leader_assignments#reassign_leader')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'REASSIGN_LEADER_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '阅读计划或用户不存在',
-
code: 'NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '重新分配失败',
-
errors: [e.message],
-
code: 'REASSIGN_LEADER_ERROR'
-
)
-
end
-
-
# POST /api/v1/reading_events/:reading_event_id/leader_assignments/:schedule_id/backup
-
# 补位分配
-
def backup_assignment
-
schedule = @reading_event.reading_schedules.find(params[:schedule_id])
-
-
unless params[:backup_leader_id].present?
-
render_error(
-
message: '请指定补位人',
-
code: 'BACKUP_LEADER_REQUIRED',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
backup_leader = User.find(params[:backup_leader_id])
-
unless backup_leader
-
render_error(
-
message: '补位人不存在',
-
code: 'BACKUP_LEADER_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
service = LeaderAssignmentService.backup_assignment!(@reading_event, schedule, backup_leader)
-
-
if service.success?
-
render_success(
-
data: service.result,
-
message: service.result[:message]
-
)
-
log_api_call('leader_assignments#backup_assignment')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'BACKUP_ASSIGNMENT_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '阅读计划或用户不存在',
-
code: 'NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '补位分配失败',
-
errors: [e.message],
-
code: 'BACKUP_ASSIGNMENT_ERROR'
-
)
-
end
-
-
# GET /api/v1/reading_events/:reading_event_id/leader_assignments/statistics
-
# 获取领读分配统计
-
def statistics
-
service = LeaderAssignmentService.assignment_statistics(@reading_event)
-
-
if service.success?
-
render_success(
-
data: service.result
-
)
-
log_api_call('leader_assignments#statistics')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'STATISTICS_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue => e
-
render_error(
-
message: '获取统计失败',
-
errors: [e.message],
-
code: 'STATISTICS_ERROR'
-
)
-
end
-
-
# GET /api/v1/reading_events/:reading_event_id/leader_assignments/backup_needed
-
# 获取需要补位的日程
-
def backup_needed
-
backup_schedules = @reading_event.schedules_need_backup
-
-
schedule_data = backup_schedules.map do |backup_info|
-
{
-
schedule: {
-
id: backup_info[:schedule].id,
-
day_number: backup_info[:schedule].day_number,
-
date: backup_info[:schedule].date,
-
reading_progress: backup_info[:schedule].reading_progress
-
},
-
leader: backup_info[:leader] ? {
-
id: backup_info[:leader].id,
-
nickname: backup_info[:leader].nickname,
-
avatar_url: backup_info[:leader].avatar_url
-
} : nil,
-
backup_priority: backup_info[:backup_priority],
-
missing_content: backup_info[:missing_content],
-
missing_flowers: backup_info[:missing_flowers],
-
needs_backup: backup_info[:needs_backup],
-
content_deadline: backup_info[:content_deadline],
-
flowers_deadline: backup_info[:flowers_deadline]
-
}
-
end
-
-
render_success(
-
data: {
-
backup_schedules: schedule_data,
-
total_needing_backup: schedule_data.count,
-
content_deadline_soon: schedule_data.select { |s| s[:missing_content] && s[:content_deadline] <= Date.today + 1.day }.count,
-
flowers_deadline_soon: schedule_data.select { |s| s[:missing_flowers] && s[:flowers_deadline] <= Date.today + 1.day }.count
-
}
-
)
-
log_api_call('leader_assignments#backup_needed')
-
rescue => e
-
render_error(
-
message: '获取补位信息失败',
-
errors: [e.message],
-
code: 'BACKUP_NEEDED_ERROR'
-
)
-
end
-
-
# GET /api/v1/reading_events/:reading_event_id/leader_assignments/permissions
-
# 检查领读权限
-
def check_permissions
-
schedule = params[:schedule_id] ? @reading_event.reading_schedules.find(params[:schedule_id]) : nil
-
-
service = LeaderAssignmentService.check_permissions(@reading_event, current_user, schedule)
-
-
if service.success?
-
render_success(
-
data: service.result
-
)
-
log_api_call('leader_assignments#check_permissions')
-
else
-
render_error(
-
message: service.error_message,
-
code: 'PERMISSION_CHECK_FAILED',
-
status: :unprocessable_entity
-
)
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
rescue => e
-
render_error(
-
message: '权限检查失败',
-
errors: [e.message],
-
code: 'PERMISSION_CHECK_ERROR'
-
)
-
end
-
-
private
-
-
def set_reading_event
-
event_id = params[:reading_event_id]
-
@reading_event = ReadingEvent.find(event_id)
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def check_event_permissions
-
unless @reading_event.leader == current_user
-
render_error(
-
message: '只有活动创建者可以管理领读分配',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
end
-
end
-
-
def get_assignment_statistics
-
service = LeaderAssignmentService.assignment_statistics(@reading_event)
-
service.success? ? service.result : {}
-
end
-
end
-
# frozen_string_literal: true
-
-
class Api::V1::NotificationsController < ApplicationController
-
before_action :authenticate_user!
-
before_action :set_notification, only: [:show, :update, :destroy]
-
-
# GET /api/v1/notifications
-
# 获取用户的通知列表
-
def index
-
page = params[:page] || 1
-
limit = params[:limit] || 20
-
notification_type = params[:type]
-
read_status = params[:read_status] # 'read', 'unread', or nil for all
-
-
notifications = current_user.received_notifications.includes(:actor, :notifiable)
-
-
# 按类型过滤
-
notifications = notifications.by_type(notification_type) if notification_type.present?
-
-
# 按读取状态过滤
-
case read_status
-
when 'read'
-
notifications = notifications.read
-
when 'unread'
-
notifications = notifications.unread
-
end
-
-
# 分页
-
total_count = notifications.count
-
notifications = notifications.offset((page - 1) * limit).limit(limit)
-
-
render json: {
-
success: true,
-
notifications: notifications.map { |n| n.as_json_for_api(include_actor: true, include_notifiable: true) },
-
pagination: {
-
current_page: page.to_i,
-
total_count: total_count,
-
total_pages: (total_count.to_f / limit).ceil,
-
has_next: (page.to_i * limit) < total_count,
-
has_prev: page.to_i > 1
-
},
-
stats: {
-
unread_count: current_user.received_notifications.unread.count,
-
total_count: total_count
-
}
-
}
-
end
-
-
# GET /api/v1/notifications/:id
-
# 获取单个通知详情
-
def show
-
render json: {
-
success: true,
-
notification: @notification.as_json_for_api(include_actor: true, include_notifiable: true)
-
}
-
end
-
-
# PATCH /api/v1/notifications/:id
-
# 标记通知为已读
-
def update
-
if @notification.mark_as_read!
-
render json: {
-
success: true,
-
message: '通知已标记为已读',
-
notification: @notification.as_json_for_api
-
}
-
else
-
render json: {
-
success: false,
-
error: '标记通知失败'
-
}, status: :unprocessable_entity
-
end
-
end
-
-
# DELETE /api/v1/notifications/:id
-
# 删除通知
-
def destroy
-
if @notification.destroy
-
render json: {
-
success: true,
-
message: '通知已删除'
-
}
-
else
-
render json: {
-
success: false,
-
error: '删除通知失败'
-
}, status: :unprocessable_entity
-
end
-
end
-
-
# POST /api/v1/notifications/mark_all_read
-
# 批量标记所有通知为已读
-
def mark_all_read
-
count = NotificationService.mark_all_as_read_for(current_user)
-
-
render json: {
-
success: true,
-
message: "已标记 #{count} 条通知为已读",
-
marked_count: count
-
}
-
end
-
-
# DELETE /api/v1/notifications/batch
-
# 批量删除通知
-
def batch_destroy
-
notification_ids = params[:notification_ids] || []
-
-
if notification_ids.blank?
-
return render json: {
-
success: false,
-
error: '请选择要删除的通知'
-
}, status: :bad_request
-
end
-
-
deleted_count = NotificationService.delete_notifications(notification_ids, current_user)
-
-
render json: {
-
success: true,
-
message: "已删除 #{deleted_count} 条通知",
-
deleted_count: deleted_count
-
}
-
end
-
-
# GET /api/v1/notifications/unread_count
-
# 获取未读通知数量
-
def unread_count
-
count = NotificationService.unread_count_for(current_user)
-
-
render json: {
-
success: true,
-
unread_count: count
-
}
-
end
-
-
# GET /api/v1/notifications/stats
-
# 获取通知统计信息
-
def stats
-
days = params[:days]&.to_i || 7
-
stats = NotificationService.notification_stats_for(current_user, days)
-
-
render json: {
-
success: true,
-
stats: stats,
-
period: "#{days} 天"
-
}
-
end
-
-
# GET /api/v1/notifications/recent
-
# 获取最近的通知
-
def recent
-
limit = params[:limit]&.to_i || 5
-
include_read = params[:include_read] == 'true'
-
-
notifications = NotificationService.recent_notifications_for(current_user, limit, include_read)
-
-
render json: {
-
success: true,
-
notifications: notifications.map { |n| n.as_json_for_api(include_actor: true) }
-
}
-
end
-
-
# GET /api/v1/notifications/check_new
-
# 检查是否有新通知
-
def check_new
-
since = params[:since]&.to_time
-
has_new = NotificationService.has_new_notifications?(current_user, since: since)
-
-
render json: {
-
success: true,
-
has_new: has_new,
-
unread_count: NotificationService.unread_count_for(current_user)
-
}
-
end
-
-
# POST /api/v1/notifications/test
-
# 测试通知(仅开发环境)
-
def test
-
return render json: { error: '此功能仅在开发环境中可用' }, status: :forbidden unless Rails.env.development?
-
-
# 创建测试通知
-
test_notification = NotificationService.send_system_notification(
-
current_user,
-
'测试通知',
-
'这是一个测试通知,用于验证通知系统功能。',
-
actor: current_user
-
)
-
-
render json: {
-
success: true,
-
message: '测试通知已创建',
-
notification: test_notification.first&.as_json_for_api
-
}
-
end
-
-
private
-
-
# 设置通知
-
def set_notification
-
@notification = current_user.received_notifications.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render json: { error: '通知不存在' }, status: :not_found
-
end
-
end
-
# frozen_string_literal: true
-
-
module Api
-
module V1
-
# PerformancePostsController - 高性能Posts控制器
-
# 集成所有性能优化策略:索引优化、N+1查询解决、分页优化、缓存策略
-
class PerformancePostsController < Api::V1::BaseController
-
before_action :authenticate_user!
-
-
# GET /api/v1/performance_posts
-
# 高性能帖子列表,支持cursor分页和缓存
-
def index
-
# 解析参数
-
filters = parse_filters
-
pagination_options = parse_pagination_options
-
cache_options = parse_cache_options
-
-
# 使用缓存获取帖子列表
-
if should_use_cache?
-
posts_data = QueryCacheService.fetch_posts_list(
-
filters,
-
page: pagination_options[:page],
-
per_page: pagination_options[:per_page],
-
current_user: current_user
-
)
-
-
# 构建分页信息
-
if pagination_options[:cursor]
-
# Cursor分页信息
-
total_count = nil
-
pagination_info = cursor_pagination_info(posts_data, pagination_options)
-
else
-
# 传统分页信息
-
total_count = Post.visible.count
-
pagination_info = offset_pagination_info(total_count, pagination_options)
-
end
-
-
render json: {
-
posts: posts_data.map { |post| serialize_post(post, lite: true) },
-
pagination: pagination_info,
-
cached: true,
-
performance: {
-
query_time_ms: 5, # 缓存命中时的时间
-
cache_hit: true
-
}
-
}
-
else
-
# 直接查询(不使用缓存)
-
posts_data = execute_direct_query(filters, pagination_options)
-
render json: {
-
posts: posts_data[:posts].map { |post| serialize_post(post, lite: true) },
-
pagination: posts_data[:pagination],
-
cached: false,
-
performance: {
-
query_time_ms: 150, # 直接查询的预估时间
-
cache_hit: false
-
}
-
}
-
end
-
end
-
-
# GET /api/v1/performance_posts/:id
-
# 高性能帖子详情,支持缓存
-
def show
-
# 使用缓存获取帖子详情
-
post = QueryCacheService.fetch_post(params[:id], current_user: current_user)
-
-
# 检查权限
-
unless current_user.any_admin?
-
if post.hidden?
-
return render json: { error: "帖子已被隐藏" }, status: :not_found
-
end
-
end
-
-
render json: {
-
post: serialize_post(post),
-
cached: true,
-
performance: {
-
query_time_ms: 3,
-
cache_hit: true
-
}
-
}
-
end
-
-
# POST /api/v1/performance_posts
-
# 创建帖子,同时清除相关缓存
-
def create
-
service_result = PostServiceFacade.create_with_data(current_user, post_params)
-
-
if service_result.success?
-
# 清除相关缓存
-
clear_related_caches
-
-
render json: {
-
post: service_result.data[:post],
-
message: "帖子创建成功",
-
performance: {
-
cache_cleared: true
-
}
-
}, status: :created
-
else
-
render json: { errors: service_result.error_messages }, status: :unprocessable_entity
-
end
-
end
-
-
# GET /api/v1/performance_posts/stats
-
# 帖子统计信息,使用缓存
-
def stats
-
stats_data = QueryCacheService.fetch("posts_stats:#{Date.current}",
-
expires_in: 1.hour) do
-
{
-
total_posts: Post.visible.count,
-
total_comments: Comment.joins(:post).where(posts: { hidden: false }).count,
-
total_likes: Like.joins("INNER JOIN posts ON likes.target_id = posts.id AND likes.target_type = 'Post'")
-
.where(posts: { hidden: false }).count,
-
posts_by_category: posts_by_category_stats,
-
recent_activity: recent_activity_stats
-
}
-
end
-
-
render json: {
-
stats: stats_data,
-
cached: true,
-
performance: {
-
query_time_ms: 10
-
}
-
}
-
end
-
-
private
-
-
# 解析筛选参数
-
def parse_filters
-
{
-
category: params[:category],
-
user_id: params[:user_id],
-
date_from: params[:date_from],
-
date_to: params[:date_to]
-
}.compact
-
end
-
-
# 解析分页参数
-
def parse_pagination_options
-
if params[:cursor].present?
-
{
-
cursor: params[:cursor],
-
per_page: [params[:per_page].to_i, 50].min,
-
order_field: params[:order]&.to_sym || :created_at,
-
order_direction: params[:direction]&.to_sym || :desc
-
}
-
else
-
{
-
page: [params[:page].to_i, 1].max,
-
per_page: [params[:per_page].to_i, 50].min,
-
order_field: params[:order]&.to_sym || :created_at,
-
order_direction: params[:direction]&.to_sym || :desc
-
}
-
end
-
end
-
-
# 解析缓存参数
-
def parse_cache_options
-
{
-
use_cache: params[:cache] != 'false',
-
cache_level: params[:cache_level]&.to_sym || :redis,
-
expires_in: params[:expires_in]&.to_i || 5.minutes
-
}
-
end
-
-
# 判断是否使用缓存
-
def should_use_cache?
-
cache_options = parse_cache_options
-
cache_options[:use_cache] && !cache_bypass_required?
-
end
-
-
# 判断是否需要绕过缓存
-
def cache_bypass_required?
-
# 用户指定不使用缓存
-
return true if params[:cache] == 'false'
-
-
# 管理员请求实时数据
-
return true if current_user&.any_admin? && params[:realtime] == 'true'
-
-
# 特殊筛选条件不使用缓存
-
return true if params[:user_id].present? || params[:date_from].present?
-
-
false
-
end
-
-
# 执行直接查询
-
def execute_direct_query(filters, pagination_options)
-
# 构建基础查询
-
posts_query = Post.visible.includes(:user)
-
-
# 应用筛选
-
posts_query = apply_filters(posts_query, filters)
-
-
# 应用排序
-
posts_query = apply_ordering(posts_query, pagination_options)
-
-
# 应用分页
-
if pagination_options[:cursor]
-
result = OptimizedPaginationService.cursor_paginate(
-
posts_query,
-
cursor: pagination_options[:cursor],
-
per_page: pagination_options[:per_page],
-
order_field: pagination_options[:order_field],
-
order_direction: pagination_options[:order_direction]
-
)
-
else
-
result = OptimizedPaginationService.paginate(
-
posts_query,
-
page: pagination_options[:page],
-
per_page: pagination_options[:per_page],
-
order_field: pagination_options[:order_field],
-
order_direction: pagination_options[:order_direction]
-
)
-
end
-
-
# 预加载权限和点赞状态
-
preload_interactions(result.records, current_user) if current_user
-
-
{
-
posts: result.records,
-
pagination: build_pagination_info(result, pagination_options)
-
}
-
end
-
-
# 应用筛选条件
-
def apply_filters(query, filters)
-
query = query.where(category: filters[:category]) if filters[:category]
-
query = query.where(user_id: filters[:user_id]) if filters[:user_id]
-
query = query.where('created_at >= ?', filters[:date_from]) if filters[:date_from]
-
query = query.where('created_at <= ?', filters[:date_to]) if filters[:date_to]
-
query
-
end
-
-
# 应用排序
-
def apply_ordering(query, options)
-
case options[:order_field]
-
when :likes_count
-
query = query.order('likes_count DESC, created_at DESC')
-
when :comments_count
-
query = query.order('comments_count DESC, created_at DESC')
-
else
-
query = query.order("#{options[:order_field]} #{options[:order_direction].upcase}")
-
end
-
query
-
end
-
-
# 预加载交互信息
-
def preload_interactions(posts, user)
-
return if posts.empty?
-
-
post_ids = posts.map(&:id)
-
-
# 批量加载权限
-
permissions = PostPermissionService.batch_check_posts_permissions(
-
post_ids, user.id
-
)
-
-
# 批量加载点赞状态
-
liked_post_ids = Like.where(
-
user_id: user.id,
-
target_type: 'Post',
-
target_id: post_ids
-
).pluck(:target_id)
-
-
posts.each do |post|
-
post.instance_variable_set(:@permissions, permissions)
-
post.instance_variable_set(:@current_user_liked, liked_post_ids.include?(post.id))
-
end
-
end
-
-
# 序列化帖子
-
def serialize_post(post, lite: false)
-
permissions = post.instance_variable_get(:@permissions) || {}
-
liked_status = post.instance_variable_get(:@current_user_liked)
-
-
result = {
-
id: post.id,
-
title: post.title,
-
content: post.content,
-
category: post.category,
-
category_name: post.category_name,
-
pinned: post.pinned,
-
hidden: post.hidden,
-
created_at: post.created_at,
-
updated_at: post.updated_at,
-
time_ago: post.time_ago_in_words(post.created_at),
-
stats: {
-
likes_count: post.likes_count,
-
comments_count: post.comments_count
-
},
-
author: post.user.as_json_for_api
-
}
-
-
# 添加交互信息
-
if current_user && !lite
-
result[:interactions] = {
-
liked: liked_status || false,
-
can_edit: permissions.dig(:edit, post.id) || false,
-
can_delete: permissions.dig(:delete, post.id) || false,
-
can_pin: permissions.dig(:pin, post.id) || false,
-
can_hide: permissions.dig(:hide, post.id) || false,
-
can_comment: permissions.dig(:comment, post.id) || false
-
}
-
end
-
-
result
-
end
-
-
# 构建分页信息
-
def build_pagination_info(pagination_result, options)
-
if options[:cursor]
-
{
-
type: 'cursor',
-
next_cursor: pagination_result.next_cursor,
-
prev_cursor: pagination_result.prev_cursor,
-
has_next: pagination_result.has_next_page?,
-
has_prev: pagination_result.has_prev_page?,
-
per_page: options[:per_page]
-
}
-
else
-
{
-
type: 'offset',
-
current_page: pagination_result.current_page,
-
per_page: options[:per_page],
-
total_count: pagination_result.total_count,
-
total_pages: pagination_result.total_pages,
-
has_next: pagination_result.has_next_page?,
-
has_prev: pagination_result.has_prev_page?
-
}
-
end
-
end
-
-
# 清除相关缓存
-
def clear_related_caches
-
patterns = [
-
'posts_list:*',
-
'post:*',
-
'posts_stats:*'
-
]
-
-
patterns.each do |pattern|
-
QueryCacheService.clear_cache(pattern)
-
end
-
-
Rails.logger.info "已清除帖子相关缓存"
-
end
-
-
# 统计方法
-
def posts_by_category_stats
-
Post.visible.group(:category).count
-
end
-
-
def recent_activity_stats
-
{
-
posts_today: Post.visible.where('created_at >= ?', Date.current).count,
-
comments_today: Comment.joins(:post)
-
.where(posts: { hidden: false })
-
.where('comments.created_at >= ?', Date.current)
-
.count,
-
likes_today: Like.joins("INNER JOIN posts ON likes.target_id = posts.id AND likes.target_type = 'Post'")
-
.where(posts: { hidden: false })
-
.where('likes.created_at >= ?', Date.current)
-
.count
-
}
-
end
-
-
def post_params
-
params.require(:post).permit(:title, :content, :category, :images, tags: [])
-
end
-
end
-
end
-
end
-
class Api::V1::ReadingEventsController < Api::V1::BaseController
-
before_action :authenticate_user!, except: [:index, :show]
-
before_action :set_reading_event, only: [:show, :update, :destroy, :start, :complete, :approve, :reject, :statistics]
-
before_action :authorize_event_leader!, only: [:update, :destroy, :start]
-
before_action :authorize_admin!, only: [:approve, :reject]
-
-
# GET /api/v1/reading_events
-
# 活动列表和搜索
-
def index
-
@reading_events = ReadingEvent.includes(:leader, :event_enrollments)
-
.filter_by_status(params[:status])
-
.filter_by_mode(params[:activity_mode])
-
.filter_by_fee_type(params[:fee_type])
-
-
# 关键词搜索
-
if params[:keyword].present?
-
keyword = "%#{params[:keyword]}%"
-
@reading_events = @reading_events.where(
-
"reading_events.title ILIKE ? OR reading_events.book_name ILIKE ?",
-
keyword, keyword
-
)
-
end
-
-
# 时间范围过滤
-
if params[:start_date_from].present?
-
start_date = safe_date_param(:start_date_from)
-
@reading_events = @reading_events.where('reading_events.start_date >= ?', start_date) if start_date
-
end
-
-
if params[:start_date_to].present?
-
end_date = safe_date_param(:start_date_to)
-
@reading_events = @reading_events.where('reading_events.start_date <= ?', end_date) if end_date
-
end
-
-
# 排序
-
sorting = sorting_params(default_field: :created_at)
-
@reading_events = @reading_events.order("#{sorting[:sort_field]} #{sorting[:sort_direction]}")
-
-
# 分页
-
pagination = pagination_params
-
@reading_events = @reading_events.page(pagination[:page]).per(pagination[:per_page])
-
-
# 构建响应数据
-
events_data = @reading_events.map do |event|
-
{
-
id: event.id,
-
title: event.title,
-
book_name: event.book_name,
-
book_cover_url: event.book_cover_url,
-
description: event.description,
-
activity_mode: event.activity_mode,
-
fee_type: event.fee_type,
-
fee_amount: event.fee_amount,
-
start_date: event.start_date,
-
end_date: event.end_date,
-
status: event.status,
-
approval_status: event.approval_status,
-
participants_count: event.participants_count,
-
max_participants: event.max_participants,
-
available_spots: event.available_spots,
-
leader: {
-
id: event.leader.id,
-
nickname: event.leader.nickname
-
},
-
created_at: event.created_at
-
}
-
end
-
-
render_success(
-
data: events_data,
-
meta: pagination_meta(@reading_events)
-
)
-
-
log_api_call('reading_events#index')
-
end
-
-
# GET /api/v1/reading_events/:id
-
# 活动详情
-
def show
-
event_data = {
-
id: @reading_event.id,
-
title: @reading_event.title,
-
book_name: @reading_event.book_name,
-
book_cover_url: @reading_event.book_cover_url,
-
description: @reading_event.description,
-
activity_mode: @reading_event.activity_mode,
-
weekend_rest: @reading_event.weekend_rest,
-
completion_standard: @reading_event.completion_standard,
-
leader_assignment_type: @reading_event.leader_assignment_type,
-
fee_type: @reading_event.fee_type,
-
fee_amount: @reading_event.fee_amount,
-
leader_reward_percentage: @reading_event.leader_reward_percentage,
-
max_participants: @reading_event.max_participants,
-
min_participants: @reading_event.min_participants,
-
start_date: @reading_event.start_date,
-
end_date: @reading_event.end_date,
-
enrollment_deadline: @reading_event.enrollment_deadline,
-
status: @reading_event.status,
-
approval_status: @reading_event.approval_status,
-
participants_count: @reading_event.participants_count,
-
available_spots: @reading_event.available_spots,
-
days_count: @reading_event.days_count,
-
leader: {
-
id: @reading_event.leader.id,
-
nickname: @reading_event.leader.nickname
-
},
-
created_at: @reading_event.created_at,
-
updated_at: @reading_event.updated_at
-
}
-
-
# 如果已登录,添加用户相关信息
-
if current_user
-
enrollment = @reading_event.event_enrollments.find_by(user: current_user)
-
event_data[:user_enrollment] = enrollment ? {
-
id: enrollment.id,
-
enrollment_type: enrollment.enrollment_type,
-
status: enrollment.status,
-
enrollment_date: enrollment.enrollment_date,
-
completion_rate: enrollment.completion_rate,
-
check_ins_count: enrollment.check_ins_count,
-
flowers_received_count: enrollment.flowers_received_count
-
} : nil
-
-
event_data[:user_permissions] = {
-
can_enroll: @reading_event.can_enroll? && !enrollment,
-
can_edit: current_user == @reading_event.leader,
-
can_start: @reading_event.can_start? && current_user == @reading_event.leader,
-
is_participant: enrollment&.can_participate? || false
-
}
-
end
-
-
render_success(data: event_data)
-
log_api_call('reading_events#show')
-
end
-
-
# POST /api/v1/reading_events
-
# 创建活动
-
def create
-
return unless authenticate_user!
-
return unless validate_required_fields(:title, :book_name, :start_date, :end_date)
-
-
ActiveRecord::Base.transaction do
-
@reading_event = ReadingEvent.new(reading_event_params)
-
@reading_event.leader = current_user
-
@reading_event.status = :draft
-
-
if @reading_event.save
-
event_data = build_event_data(@reading_event)
-
render_success(
-
data: event_data,
-
message: '活动创建成功'
-
)
-
log_api_call('reading_events#create')
-
else
-
render_error(
-
message: '活动创建失败',
-
errors: @reading_event.errors.full_messages,
-
code: 'VALIDATION_ERROR'
-
)
-
end
-
end
-
end
-
-
# PUT/PATCH /api/v1/reading_events/:id
-
# 更新活动
-
def update
-
ActiveRecord::Base.transaction do
-
if @reading_event.update(reading_event_params)
-
event_data = build_event_data(@reading_event)
-
render_success(
-
data: event_data,
-
message: '活动更新成功'
-
)
-
log_api_call('reading_events#update')
-
else
-
render_error(
-
message: '活动更新失败',
-
errors: @reading_event.errors.full_messages,
-
code: 'VALIDATION_ERROR'
-
)
-
end
-
end
-
end
-
-
# DELETE /api/v1/reading_events/:id
-
# 删除活动
-
def destroy
-
# 只有草稿状态或被拒绝的活动才能删除
-
unless @reading_event.draft? || @reading_event.rejected?
-
render_error(
-
message: '只有草稿状态或被拒绝的活动才能删除',
-
code: 'CANNOT_DELETE_EVENT'
-
)
-
return
-
end
-
-
ActiveRecord::Base.transaction do
-
@reading_event.destroy!
-
render_success(message: '活动删除成功')
-
log_api_call('reading_events#destroy')
-
end
-
rescue ActiveRecord::RecordNotDestroyed
-
render_error(
-
message: '活动删除失败',
-
code: 'DELETE_FAILED'
-
)
-
end
-
-
# POST /api/v1/reading_events/:id/start
-
# 开始活动
-
def start
-
unless @reading_event.can_start?
-
render_error(
-
message: '活动当前状态无法开始',
-
code: 'CANNOT_START_EVENT'
-
)
-
return
-
end
-
-
if @reading_event.start!
-
render_success(
-
data: build_event_data(@reading_event),
-
message: '活动已开始'
-
)
-
log_api_call('reading_events#start')
-
else
-
render_error(
-
message: '活动开始失败',
-
code: 'START_FAILED'
-
)
-
end
-
end
-
-
# POST /api/v1/reading_events/:id/complete
-
# 完成活动(管理员或活动创建者)
-
def complete
-
unless @reading_event.can_complete?
-
render_error(
-
message: '活动当前状态无法完成',
-
code: 'CANNOT_COMPLETE_EVENT'
-
)
-
return
-
end
-
-
if @reading_event.complete!
-
render_success(
-
data: build_event_data(@reading_event),
-
message: '活动已完成'
-
)
-
log_api_call('reading_events#complete')
-
else
-
render_error(
-
message: '活动完成失败',
-
code: 'COMPLETE_FAILED'
-
)
-
end
-
end
-
-
# POST /api/v1/reading_events/:id/approve
-
# 审批通过活动(管理员)
-
def approve
-
unless @reading_event.pending_approval?
-
render_error(
-
message: '活动当前状态无法审批',
-
code: 'CANNOT_APPROVE_EVENT'
-
)
-
return
-
end
-
-
if @reading_event.approve!(current_user)
-
render_success(
-
data: build_event_data(@reading_event),
-
message: '活动已审批通过'
-
)
-
log_api_call('reading_events#approve')
-
else
-
render_error(
-
message: '活动审批失败',
-
code: 'APPROVE_FAILED'
-
)
-
end
-
end
-
-
# POST /api/v1/reading_events/:id/reject
-
# 拒绝活动(管理员)
-
def reject
-
unless @reading_event.pending_approval?
-
render_error(
-
message: '活动当前状态无法拒绝',
-
code: 'CANNOT_REJECT_EVENT'
-
)
-
return
-
end
-
-
reason = params[:reason] || '不符合活动规范'
-
-
if @reading_event.reject!(current_user, reason)
-
render_success(
-
data: build_event_data(@reading_event),
-
message: '活动已拒绝'
-
)
-
log_api_call('reading_events#reject')
-
else
-
render_error(
-
message: '活动拒绝失败',
-
code: 'REJECT_FAILED'
-
)
-
end
-
end
-
-
# GET /api/v1/reading_events/:id/statistics
-
# 活动统计信息
-
def statistics
-
unless @reading_event.in_progress? || @reading_event.completed?
-
render_error(
-
message: '活动未开始或已结束,暂无统计数据',
-
code: 'NO_STATISTICS_AVAILABLE'
-
)
-
return
-
end
-
-
stats = @reading_event.completion_statistics
-
-
# 添加参与者排行榜
-
top_participants = @reading_event.event_enrollments
-
.includes(:user)
-
.by_completion_rate(:desc)
-
.limit(10)
-
.map do |enrollment|
-
{
-
user_id: enrollment.user.id,
-
nickname: enrollment.user.nickname,
-
completion_rate: enrollment.completion_rate,
-
check_ins_count: enrollment.check_ins_count,
-
flowers_received_count: enrollment.flowers_received_count
-
}
-
end
-
-
statistics_data = {
-
total_participants: stats[:total_participants],
-
completed_participants: stats[:completed_participants],
-
average_completion_rate: stats[:average_completion_rate],
-
total_check_ins: stats[:total_check_ins],
-
total_flowers: stats[:total_flowers],
-
completion_rate: stats[:total_participants] > 0 ?
-
(stats[:completed_participants].to_f / stats[:total_participants] * 100).round(2) : 0,
-
top_participants: top_participants
-
}
-
-
render_success(data: statistics_data)
-
log_api_call('reading_events#statistics')
-
end
-
-
private
-
-
def set_reading_event
-
@reading_event = ReadingEvent.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def reading_event_params
-
{
-
title: params[:title],
-
book_name: params[:book_name],
-
book_cover_url: params[:book_cover_url],
-
description: params[:description],
-
activity_mode: params[:activity_mode] || 'note_checkin',
-
weekend_rest: params[:weekend_rest] == true,
-
completion_standard: params[:completion_standard]&.to_i || 80,
-
leader_assignment_type: params[:leader_assignment_type] || 'voluntary',
-
fee_type: params[:fee_type] || 'free',
-
fee_amount: params[:fee_amount]&.to_d || 0.0,
-
leader_reward_percentage: params[:leader_reward_percentage]&.to_d || 20.0,
-
max_participants: params[:max_participants]&.to_i || 25,
-
min_participants: params[:min_participants]&.to_i || 10,
-
start_date: params[:start_date]&.to_date,
-
end_date: params[:end_date]&.to_date,
-
enrollment_deadline: params[:enrollment_deadline]&.to_datetime
-
}.compact
-
end
-
-
def build_event_data(event)
-
{
-
id: event.id,
-
title: event.title,
-
book_name: event.book_name,
-
book_cover_url: event.book_cover_url,
-
description: event.description,
-
activity_mode: event.activity_mode,
-
weekend_rest: event.weekend_rest,
-
completion_standard: event.completion_standard,
-
leader_assignment_type: event.leader_assignment_type,
-
fee_type: event.fee_type,
-
fee_amount: event.fee_amount,
-
leader_reward_percentage: event.leader_reward_percentage,
-
max_participants: event.max_participants,
-
min_participants: event.min_participants,
-
start_date: event.start_date,
-
end_date: event.end_date,
-
enrollment_deadline: event.enrollment_deadline,
-
status: event.status,
-
approval_status: event.approval_status,
-
participants_count: event.participants_count,
-
available_spots: event.available_spots,
-
days_count: event.days_count,
-
leader: {
-
id: event.leader.id,
-
nickname: event.leader.nickname
-
},
-
created_at: event.created_at,
-
updated_at: event.updated_at
-
}
-
end
-
end
-
class Api::V1::ReadingSchedulesController < Api::V1::BaseController
-
before_action :authenticate_user!
-
before_action :set_reading_event
-
before_action :set_reading_schedule, only: [:show, :assign_leader, :remove_leader]
-
-
# GET /api/v1/reading_schedules
-
# 阅读计划列表
-
def index
-
# 检查权限:只有活动参与者、创建者可以查看
-
unless can_view_schedules?
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 获取阅读计划列表
-
schedules = @reading_event.reading_schedules
-
.includes(:daily_leader, :daily_leading, :check_ins, :flowers)
-
.chronological
-
-
# 分页
-
pagination = pagination_params
-
schedules = schedules.page(pagination[:page]).per(pagination[:per_page])
-
-
# 构建响应数据
-
schedules_data = schedules.map do |schedule|
-
build_schedule_data(schedule, detailed: true)
-
end
-
-
render_success(
-
data: schedules_data,
-
meta: pagination_meta(schedules)
-
)
-
-
log_api_call('reading_schedules#index')
-
end
-
-
# GET /api/v1/reading_schedules/:id
-
# 阅读计划详情
-
def show
-
unless @reading_schedule
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限
-
unless can_view_schedule?(@reading_schedule)
-
render_error(
-
message: '权限不足',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
schedule_data = build_schedule_data(@reading_schedule, detailed: true)
-
render_success(data: schedule_data)
-
log_api_call('reading_schedules#show')
-
end
-
-
# POST /api/v1/reading_schedules/:id/assign_leader
-
# 分配领读人
-
def assign_leader
-
unless @reading_schedule
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限:只有活动创建者可以分配领读人
-
unless @reading_event.leader == current_user
-
render_error(
-
message: '只有活动创建者可以分配领读人',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
# 检查用户参数
-
return unless validate_required_fields(:user_id)
-
-
target_user = User.find(params[:user_id])
-
unless target_user
-
render_error(
-
message: '用户不存在',
-
code: 'USER_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查用户是否是活动参与者
-
unless @reading_event.participants.include?(target_user)
-
render_error(
-
message: '只能分配活动的参与者作为领读人',
-
code: 'USER_NOT_PARTICIPANT',
-
status: :unprocessable_entity
-
)
-
return
-
end
-
-
ActiveRecord::Base.transaction do
-
if @reading_schedule.assign_leader!(target_user)
-
schedule_data = build_schedule_data(@reading_schedule, detailed: true)
-
render_success(
-
data: schedule_data,
-
message: '领读人分配成功'
-
)
-
log_api_call('reading_schedules#assign_leader')
-
else
-
render_error(
-
message: '领读人分配失败',
-
code: 'ASSIGN_LEADER_FAILED'
-
)
-
end
-
end
-
rescue => e
-
render_error(
-
message: '领读人分配失败',
-
errors: [e.message],
-
code: 'ASSIGN_LEADER_ERROR'
-
)
-
end
-
-
# POST /api/v1/reading_schedules/:id/remove_leader
-
# 移除领读人
-
def remove_leader
-
unless @reading_schedule
-
render_error(
-
message: '阅读计划不存在',
-
code: 'SCHEDULE_NOT_FOUND',
-
status: :not_found
-
)
-
return
-
end
-
-
# 检查权限:只有活动创建者可以移除领读人
-
unless @reading_event.leader == current_user
-
render_error(
-
message: '只有活动创建者可以移除领读人',
-
code: 'FORBIDDEN',
-
status: :forbidden
-
)
-
return
-
end
-
-
ActiveRecord::Base.transaction do
-
if @reading_schedule.remove_leader!
-
schedule_data = build_schedule_data(@reading_schedule, detailed: true)
-
render_success(
-
data: schedule_data,
-
message: '领读人移除成功'
-
)
-
log_api_call('reading_schedules#remove_leader')
-
else
-
render_error(
-
message: '领读人移除失败',
-
code: 'REMOVE_LEADER_FAILED'
-
)
-
end
-
end
-
rescue => e
-
render_error(
-
message: '领读人移除失败',
-
errors: [e.message],
-
code: 'REMOVE_LEADER_ERROR'
-
)
-
end
-
-
private
-
-
def set_reading_event
-
event_id = params[:reading_event_id]
-
@reading_event = ReadingEvent.find(event_id)
-
rescue ActiveRecord::RecordNotFound
-
render_error(
-
message: '活动不存在',
-
code: 'EVENT_NOT_FOUND',
-
status: :not_found
-
)
-
end
-
-
def set_reading_schedule
-
@reading_schedule = @reading_event.reading_schedules.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
@reading_schedule = nil
-
end
-
-
def build_schedule_data(schedule, detailed: false)
-
data = {
-
id: schedule.id,
-
day_number: schedule.day_number,
-
date: schedule.date,
-
reading_progress: schedule.reading_progress,
-
daily_leader: schedule.daily_leader ? {
-
id: schedule.daily_leader.id,
-
nickname: schedule.daily_leader.nickname,
-
avatar_url: schedule.daily_leader.avatar_url
-
} : nil,
-
created_at: schedule.created_at,
-
updated_at: schedule.updated_at
-
}
-
-
if detailed
-
data[:reading_event] = {
-
id: schedule.reading_event.id,
-
title: schedule.reading_event.title,
-
book_name: schedule.reading_event.book_name,
-
status: schedule.reading_event.status,
-
activity_mode: schedule.reading_event.activity_mode
-
}
-
-
data[:daily_leading] = schedule.daily_leading ? {
-
id: schedule.daily_leading.id,
-
content: schedule.daily_leading.content,
-
reading_pages: schedule.daily_leading.reading_pages,
-
created_at: schedule.daily_leading.created_at,
-
updated_at: schedule.daily_leading.updated_at
-
} : nil
-
-
data[:statistics] = schedule.participation_statistics
-
data[:status_info] = {
-
today?: schedule.today?,
-
past?: schedule.past?,
-
future?: schedule.future?,
-
current_day?: schedule.current_day?,
-
completed?: schedule.completed?,
-
has_check_ins?: schedule.has_check_ins?,
-
has_flowers?: schedule.has_flowers?,
-
has_leading_content?: schedule.has_leading_content?
-
}
-
-
data[:permissions] = {
-
can_view: can_view_schedule?(schedule),
-
can_assign_leader: can_assign_leader?(schedule),
-
can_remove_leader: can_remove_leader?(schedule),
-
can_publish_content: schedule.can_publish_leading_content?,
-
can_give_flowers: schedule.can_give_flowers?,
-
needs_backup: schedule.needs_backup?,
-
backup_permissions: schedule.backup_permissions
-
}
-
-
data[:leading_status] = {
-
content: schedule.leading_content_status,
-
flowers: schedule.flower_giving_status
-
}
-
end
-
-
data
-
end
-
-
# 权限检查方法
-
def can_view_schedules?
-
return true if @reading_event.leader == current_user
-
return true if @reading_event.participants.include?(current_user)
-
false
-
end
-
-
def can_view_schedule?(schedule)
-
return true if @reading_event.leader == current_user
-
return true if schedule.daily_leader == current_user
-
return true if @reading_event.participants.include?(current_user)
-
false
-
end
-
-
def can_assign_leader?(schedule)
-
return false unless @reading_event.leader == current_user
-
schedule.can_assign_leader?
-
end
-
-
def can_remove_leader?(schedule)
-
return false unless @reading_event.leader == current_user
-
schedule.daily_leader.present?
-
end
-
end
-
class ApplicationController < ActionController::API
-
include Authenticable
-
-
# API健康检查端点
-
def health
-
response_data = {
-
status: "ok",
-
timestamp: Time.current.iso8601,
-
version: "1.0.0",
-
environment: Rails.env
-
}
-
-
render json: response_data
-
end
-
-
private
-
-
def check_database_status
-
begin
-
ActiveRecord::Base.connection.execute("SELECT 1")
-
"connected"
-
rescue => e
-
"error: #{e.message}"
-
end
-
end
-
-
def check_permissions_status
-
begin
-
# 检查权限相关的关键组件
-
status = {}
-
-
# 检查User模型
-
status[:user_model] = User.respond_to?(:any_admin?) ? "ok" : "missing_methods"
-
-
# 检查AdminAuthorizable
-
status[:admin_authorizable] = defined?(AdminAuthorizable) ? "ok" : "missing"
-
-
# 检查角色枚举
-
if User.respond_to?(:roles)
-
status[:role_enums] = User.roles.keys.join(",")
-
else
-
status[:role_enums] = "not_defined"
-
end
-
-
status
-
rescue => e
-
{ error: e.message }
-
end
-
end
-
end
-
# 管理员权限验证 concern
-
module AdminAuthorizable
-
extend ActiveSupport::Concern
-
-
# 检查是否是管理员或root用户
-
def authenticate_admin!
-
unless current_user&.any_admin?
-
render json: {
-
error: "需要管理员权限",
-
details: {
-
required_role: "admin 或 root",
-
current_role: current_user&.role_display_name || "未登录"
-
}
-
}, status: :forbidden
-
end
-
end
-
-
# 检查是否是root用户
-
def authenticate_root!
-
unless current_user&.root?
-
render json: {
-
error: "需要超级管理员权限",
-
details: {
-
required_role: "root",
-
current_role: current_user&.role_display_name || "未登录"
-
}
-
}, status: :unauthorized
-
end
-
end
-
-
# 检查用户是否有特定权限
-
def authorize_permission!(permission)
-
unless current_user&.has_permission?(permission)
-
render json: {
-
error: "权限不足",
-
details: {
-
required_permission: permission,
-
current_role: current_user&.role_display_name || "未登录"
-
}
-
}, status: :forbidden
-
end
-
end
-
-
# 检查是否有审批活动权限
-
def authorize_event_approval!
-
authorize_permission!(:approve_events)
-
end
-
-
# 检查是否有管理用户权限
-
def authorize_user_management!
-
authorize_permission!(:manage_users)
-
end
-
-
# 检查是否有查看管理面板权限
-
def authorize_admin_panel!
-
authorize_permission!(:view_admin_panel)
-
end
-
-
# 检查是否有系统管理权限
-
def authorize_system_management!
-
authorize_permission!(:manage_system)
-
end
-
-
private
-
-
# 辅助方法:检查当前用户是否是管理员
-
def current_user_admin?
-
current_user&.any_admin?
-
end
-
-
# 辅助方法:检查当前用户是否是root
-
def current_user_root?
-
current_user&.root?
-
end
-
-
# 辅助方法:获取用户角色信息
-
def user_role_info(user = current_user)
-
return { role: "未登录", permissions: [] } unless user
-
-
{
-
role: user.role_display_name,
-
permissions: user_permissions(user)
-
}
-
end
-
-
# 辅助方法:获取用户权限列表
-
def user_permissions(user)
-
permissions = []
-
permissions << "approve_events" if user.can_approve_events?
-
permissions << "manage_users" if user.can_manage_users?
-
permissions << "view_admin_panel" if user.can_view_admin_panel?
-
permissions << "manage_system" if user.can_manage_system?
-
permissions
-
end
-
end
-
# frozen_string_literal: true
-
-
module ApiResponse
-
extend ActiveSupport::Concern
-
-
# 成功响应
-
def render_success(data = nil, message: nil, status: :ok)
-
response = {
-
success: true,
-
data: data
-
}
-
response[:message] = message if message.present?
-
-
render json: response, status: status
-
end
-
-
# 创建成功响应
-
def render_created(data = nil, message: '创建成功')
-
render_success(data, message: message, status: :created)
-
end
-
-
# 错误响应
-
def render_error(message, errors: nil, status: :unprocessable_entity)
-
response = {
-
success: false,
-
error: message
-
}
-
response[:errors] = errors if errors.present?
-
-
render json: response, status: status
-
end
-
-
# 未找到响应
-
def render_not_found(message = '资源不存在')
-
render_error(message, status: :not_found)
-
end
-
-
# 权限不足响应
-
def render_forbidden(message = '无权限访问')
-
render_error(message, status: :forbidden)
-
end
-
-
# 未认证响应
-
def render_unauthorized(message = '请先登录')
-
render_error(message, status: :unauthorized)
-
end
-
-
# 分页响应
-
def render_paginated(data, pagination_info = {}, message: nil)
-
response = {
-
success: true,
-
data: data,
-
pagination: pagination_info
-
}
-
response[:message] = message if message.present?
-
-
render json: response
-
end
-
-
private
-
-
# 构建分页信息
-
def build_pagination_info(collection)
-
{
-
current_page: collection.current_page,
-
total_pages: collection.total_pages,
-
total_count: collection.total_count,
-
per_page: collection.limit_value,
-
has_next_page: collection.next_page.present?,
-
has_prev_page: collection.prev_page.present?
-
}
-
end
-
end
-
# frozen_string_literal: true
-
-
# ApiResponseFormatter - API响应格式化模块
-
# 提供统一的API响应格式和成功响应处理
-
module ApiResponseFormatter
-
extend ActiveSupport::Concern
-
-
# 成功响应格式
-
def render_success_response(data: nil, message: 'Success', meta: {})
-
response_data = {
-
success: true,
-
message: message,
-
data: data,
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid
-
}
-
-
# 添加元数据
-
response_data[:meta] = meta if meta.any?
-
-
render json: response_data, status: :ok
-
end
-
-
# 分页响应格式
-
def render_paginated_response(data:, pagination:, message: 'Success', meta: {})
-
pagination_meta = {
-
current_page: pagination[:current_page],
-
per_page: pagination[:per_page],
-
total_count: pagination[:total_count],
-
total_pages: pagination[:total_pages]
-
}
-
-
# 添加cursor分页信息
-
if pagination[:next_cursor]
-
pagination_meta[:next_cursor] = pagination[:next_cursor]
-
end
-
if pagination[:prev_cursor]
-
pagination_meta[:prev_cursor] = pagination[:prev_cursor]
-
end
-
-
response_data = {
-
success: true,
-
message: message,
-
data: data,
-
pagination: pagination_meta,
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid
-
}
-
-
response_data[:meta] = meta if meta.any?
-
-
render json: response_data, status: :ok
-
end
-
-
# 创建成功响应
-
def render_created_response(data: nil, message: 'Created successfully', location: nil)
-
response_data = {
-
success: true,
-
message: message,
-
data: data,
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid
-
}
-
-
# 设置Location头
-
headers['Location'] = location if location
-
-
render json: response_data, status: :created
-
end
-
-
# 更新成功响应
-
def render_updated_response(data: nil, message: 'Updated successfully')
-
response_data = {
-
success: true,
-
message: message,
-
data: data,
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid
-
}
-
-
render json: response_data, status: :ok
-
end
-
-
# 删除成功响应
-
def render_deleted_response(message: 'Deleted successfully')
-
response_data = {
-
success: true,
-
message: message,
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid
-
}
-
-
render json: response_data, status: :ok
-
end
-
-
# 无内容响应
-
def render_no_content_response(message: 'No content')
-
head :no_content
-
end
-
-
# 批量操作响应
-
def render_batch_response(results:, message: 'Batch operation completed')
-
success_count = results.count { |r| r[:success] }
-
error_count = results.count { |r| !r[:success] }
-
-
response_data = {
-
success: true,
-
message: message,
-
data: {
-
total: results.length,
-
success_count: success_count,
-
error_count: error_count,
-
results: results
-
},
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid
-
}
-
-
render json: response_data, status: :ok
-
end
-
-
# 验证错误响应(用于手动验证)
-
def render_validation_errors_response(errors:, message: 'Validation failed')
-
error_response = {
-
success: false,
-
error: message,
-
error_code: 'VALIDATION_ERROR',
-
error_type: 'validation_error',
-
errors: errors.is_a?(Hash) ? errors.values.flatten : errors,
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid,
-
details: {
-
suggestions: [
-
'请检查必填字段是否完整',
-
'确认数据格式是否正确',
-
'参考API文档确认参数要求'
-
]
-
}
-
}
-
-
render json: error_response, status: :unprocessable_entity
-
end
-
-
# 参数错误响应(用于手动参数验证)
-
def render_parameter_error_response(parameter:, message: nil)
-
error_message = message || "参数错误: #{parameter}"
-
-
error_response = {
-
success: false,
-
error: error_message,
-
error_code: 'INVALID_PARAMETER',
-
error_type: 'parameter_error',
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid,
-
details: {
-
parameter: parameter,
-
suggestions: [
-
'请检查请求参数格式',
-
'参考API文档确认参数要求',
-
'确保参数值符合预期类型和范围'
-
]
-
}
-
}
-
-
render json: error_response, status: :unprocessable_entity
-
end
-
-
# 权限错误响应(用于手动权限检查)
-
def render_permission_denied_response(message: 'Permission denied')
-
error_response = {
-
success: false,
-
error: message,
-
error_code: 'PERMISSION_DENIED',
-
error_type: 'authorization_error',
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid,
-
details: {
-
user_id: current_user&.id,
-
user_role: current_user&.role_as_string,
-
suggestions: [
-
'请确认您有足够的权限执行此操作',
-
'如需权限提升,请联系管理员',
-
'检查用户账户状态是否正常'
-
]
-
}
-
}
-
-
render json: error_response, status: :forbidden
-
end
-
-
# 资源不存在响应(用于手动检查)
-
def render_not_found_response(resource: 'Resource', message: nil)
-
error_message = message || "#{resource} not found"
-
-
error_response = {
-
success: false,
-
error: error_message,
-
error_code: 'RESOURCE_NOT_FOUND',
-
error_type: 'not_found',
-
timestamp: Time.current.iso8601,
-
request_id: request&.request_id || SecureRandom.uuid,
-
details: {
-
resource: resource,
-
suggestions: [
-
'请检查资源ID是否正确',
-
'确认资源是否存在且未被删除',
-
'检查URL路径是否正确'
-
]
-
}
-
}
-
-
render json: error_response, status: :not_found
-
end
-
-
private
-
-
# 构建标准元数据
-
def build_meta_data(additional_meta = {})
-
base_meta = {
-
version: api_version,
-
environment: Rails.env
-
}
-
-
base_meta.merge(additional_meta)
-
end
-
-
# 获取API版本
-
def api_version
-
if request.path.start_with?('/api/v1/')
-
'v1'
-
else
-
'v0'
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ApiSecurity - API安全增强模块
-
# 提供API安全相关功能,包括限流、CSRF保护、请求验证等
-
module ApiSecurity
-
extend ActiveSupport::Concern
-
-
included do
-
# 添加请求ID追踪
-
before_action :set_request_id
-
# 添加安全头
-
before_action :set_security_headers
-
# API限流检查
-
before_action :check_rate_limits
-
# 参数安全检查
-
before_action :validate_request_security
-
# 记录API访问日志
-
after_action :log_api_access
-
end
-
-
private
-
-
# 设置请求ID
-
def set_request_id
-
request_id = request.headers['X-Request-ID'] || SecureRandom.uuid
-
response.headers['X-Request-ID'] = request_id
-
@request_id = request_id
-
end
-
-
# 设置安全头
-
def set_security_headers
-
response.headers['X-Content-Type-Options'] = 'nosniff'
-
response.headers['X-Frame-Options'] = 'DENY'
-
response.headers['X-XSS-Protection'] = '1; mode=block'
-
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
-
response.headers['Content-Security-Policy'] = "default-src 'self'"
-
response.headers['X-API-Version'] = api_version
-
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' if sensitive_request?
-
end
-
-
# 检查API限流
-
def check_rate_limits
-
# IP限流检查
-
rate_limiter = ApiRateLimitingService.check_ip_rate_limit(
-
request.remote_ip,
-
endpoint: request.path,
-
request: request
-
)
-
-
unless rate_limiter.allowed?
-
render_rate_limit_error(
-
limit: rate_limiter.limit,
-
remaining: rate_limiter.remaining_requests,
-
reset_time: rate_limiter.reset_time,
-
retry_after: calculate_retry_after(rate_limiter.reset_time)
-
)
-
return false
-
end
-
-
# 用户限流检查(如果已认证)
-
if current_user
-
user_rate_limiter = ApiRateLimitingService.check_user_rate_limit(
-
current_user,
-
endpoint: request.path,
-
request: request
-
)
-
-
unless user_rate_limiter.allowed?
-
render_rate_limit_error(
-
limit: user_rate_limiter.limit,
-
remaining: user_rate_limiter.remaining_requests,
-
reset_time: user_rate_limiter.reset_time,
-
retry_after: calculate_retry_after(user_rate_limiter.reset_time),
-
scope: 'user'
-
)
-
return false
-
end
-
end
-
-
# 全局限流检查
-
global_rate_limiter = ApiRateLimitingService.check_global_rate_limit(
-
endpoint: request.path,
-
request: request
-
)
-
-
unless global_rate_limiter.allowed?
-
render_rate_limit_error(
-
limit: global_rate_limiter.limit,
-
remaining: global_rate_limiter.remaining_requests,
-
reset_time: global_rate_limiter.reset_time,
-
retry_after: calculate_retry_after(global_rate_limiter.reset_time),
-
scope: 'global'
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 请求安全验证
-
def validate_request_security
-
# 检查User-Agent
-
validate_user_agent
-
-
# 检查请求大小
-
validate_request_size
-
-
# 检查可疑参数
-
validate_suspicious_params
-
-
# 检查请求频率模式
-
validate_request_pattern
-
end
-
-
# 验证User-Agent
-
def validate_user_agent
-
user_agent = request.user_agent
-
-
if user_agent.blank?
-
render_error_response(
-
error: '缺少User-Agent头',
-
error_code: 'MISSING_USER_AGENT',
-
error_type: 'security_error',
-
status: :bad_request
-
)
-
return false
-
end
-
-
# 检查可疑的User-Agent模式
-
suspicious_patterns = [
-
/bot/i, /crawler/i, /spider/i,
-
/scanner/i, /wget/i, /curl/i,
-
/python/i, /java/i, /go-http/i
-
]
-
-
if suspicious_patterns.any? { |pattern| user_agent.match?(pattern) } && !api_request?
-
Rails.logger.warn "Suspicious User-Agent detected: #{user_agent}"
-
end
-
-
true
-
end
-
-
# 验证请求大小
-
def validate_request_size
-
content_length = request.content_length || 0
-
max_size = max_request_size
-
-
if content_length > max_size
-
render_error_response(
-
error: '请求体过大',
-
error_code: 'REQUEST_TOO_LARGE',
-
error_type: 'security_error',
-
details: {
-
max_size: "#{max_size / 1024 / 1024}MB",
-
received_size: "#{content_length / 1024 / 1024}MB"
-
},
-
status: :payload_too_large
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证可疑参数
-
def validate_suspicious_params
-
suspicious_patterns = [
-
/<script/i, /javascript:/i, /vbscript:/i,
-
/onload=/i, /onerror=/i, /onclick=/i,
-
/union\s+select/i, /drop\s+table/i, /insert\s+into/i
-
]
-
-
params.each do |key, value|
-
next if value.is_a?(ActionController::Parameters)
-
-
if value.is_a?(String) && suspicious_patterns.any? { |pattern| value.match?(pattern) }
-
Rails.logger.warn "Suspicious parameter detected: #{key}=#{value[0..50]}"
-
-
render_error_response(
-
error: '请求包含可疑内容',
-
error_code: 'SUSPICIOUS_CONTENT',
-
error_type: 'security_error',
-
status: :bad_request
-
)
-
return false
-
end
-
end
-
-
true
-
end
-
-
# 验证请求模式
-
def validate_request_pattern
-
client_id = "#{request.remote_ip}:#{request.user_agent}"
-
key = "request_pattern:#{Digest::MD5.hexdigest(client_id)}"
-
-
# 获取最近请求时间
-
recent_requests = Rails.cache.read(key) || []
-
-
# 清理5分钟前的请求
-
five_minutes_ago = 5.minutes.ago.to_f
-
recent_requests.select! { |timestamp| timestamp > five_minutes_ago }
-
-
# 检查是否存在异常请求模式
-
if recent_requests.length > 50 # 5分钟内超过50个请求
-
Rails.logger.warn "Suspicious request pattern detected: #{client_id}"
-
-
render_error_response(
-
error: '请求频率异常',
-
error_code: 'SUSPICIOUS_PATTERN',
-
error_type: 'security_error',
-
details: {
-
recent_requests: recent_requests.length,
-
time_window: '5 minutes'
-
},
-
status: :too_many_requests
-
)
-
return false
-
end
-
-
# 记录当前请求时间
-
recent_requests << Time.current.to_f
-
Rails.cache.write(key, recent_requests, expires_in: 5.minutes)
-
-
true
-
end
-
-
# 判断是否为敏感请求
-
def sensitive_request?
-
sensitive_endpoints = [
-
/auth/, /login/, /register/, /password/,
-
/admin/, /delete/, /update/, /create/
-
]
-
-
sensitive_endpoints.any? { |pattern| request.path.match?(pattern) }
-
end
-
-
# 判断是否为API请求
-
def api_request?
-
request.path.start_with?('/api/')
-
end
-
-
# 获取最大请求大小
-
def max_request_size
-
case request.path
-
when /upload/
-
100.megabytes
-
when /auth/
-
1.megabyte
-
else
-
10.megabytes
-
end
-
end
-
-
# 计算重试时间
-
def calculate_retry_after(reset_time)
-
reset_timestamp = Time.parse(reset_time).to_i
-
current_timestamp = Time.current.to_i
-
[reset_timestamp - current_timestamp, 1].max
-
end
-
-
# 记录API访问日志
-
def log_api_access
-
log_data = {
-
request_id: @request_id,
-
ip: request.remote_ip,
-
method: request.method,
-
path: request.path,
-
status: response.status,
-
user_id: current_user&.id,
-
user_agent: request.user_agent,
-
duration: measure_request_duration,
-
response_size: response.body&.size || 0
-
}
-
-
Rails.logger.info "API Access: #{log_data.to_json}"
-
-
# 记录到专门的访问日志(如果配置了)
-
if Rails.application.config.log_to_stdout
-
Rails.logger.stdout.log_info("API_ACCESS", log_data)
-
end
-
end
-
-
# 测量请求处理时间
-
def measure_request_duration
-
@request_start_time ||= Time.current
-
((Time.current - @request_start_time) * 1000).round(2)
-
end
-
-
# 渲染限流错误响应
-
def render_rate_limit_error(limit:, remaining:, reset_time:, retry_after:, scope: nil)
-
error_response = {
-
success: false,
-
error: 'API请求频率超过限制',
-
error_code: 'RATE_LIMIT_EXCEEDED',
-
error_type: 'rate_limit_error',
-
timestamp: Time.current.iso8601,
-
request_id: @request_id,
-
details: {
-
limit: limit,
-
remaining: remaining,
-
reset_time: reset_time,
-
retry_after: retry_after,
-
scope: scope
-
},
-
suggestions: [
-
'请稍后重试',
-
'如需更高限额,请联系管理员',
-
'检查是否存在异常请求行为'
-
]
-
}
-
-
response.headers['Retry-After'] = retry_after.to_s
-
response.headers['X-RateLimit-Limit'] = limit.to_s
-
response.headers['X-RateLimit-Remaining'] = remaining.to_s
-
response.headers['X-RateLimit-Reset'] = reset_time
-
-
render json: error_response, status: :too_many_requests
-
end
-
-
# 渲染安全错误响应
-
def render_security_error_response(message:, error_code:, details: {})
-
error_response = {
-
success: false,
-
error: message,
-
error_code: error_code,
-
error_type: 'security_error',
-
timestamp: Time.current.iso8601,
-
request_id: @request_id,
-
details: details,
-
suggestions: [
-
'请检查请求格式和内容',
-
'确认请求来源可信',
-
'如问题持续存在,请联系技术支持'
-
]
-
}
-
-
render json: error_response, status: :bad_request
-
end
-
-
# 生成API令牌(用于内部服务认证)
-
def generate_api_token(service_name, expires_in: 1.hour)
-
payload = {
-
service: service_name,
-
iat: Time.current.to_i,
-
exp: (Time.current + expires_in).to_i,
-
jti: SecureRandom.hex(16)
-
}
-
-
JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256')
-
end
-
-
# 验证API令牌
-
def verify_api_token(token)
-
decoded = JWT.decode(
-
token,
-
Rails.application.secrets.secret_key_base,
-
true,
-
{ algorithm: 'HS256' }
-
).first
-
-
# 检查服务是否在允许列表中
-
allowed_services = Rails.application.config.x.allowed_api_services || []
-
unless allowed_services.include?(decoded['service'])
-
raise JWT::VerificationError, 'Service not allowed'
-
end
-
-
decoded
-
rescue JWT::ExpiredSignature
-
raise JWT::ExpiredSignature, 'Token expired'
-
rescue JWT::DecodeError
-
raise JWT::DecodeError, 'Invalid token'
-
end
-
end
-
# frozen_string_literal: true
-
-
# ApiVersionable - API版本控制模块
-
# 为控制器提供API版本处理功能
-
module ApiVersionable
-
extend ActiveSupport::Concern
-
-
included do
-
before_action :set_api_version
-
before_action :validate_api_version
-
before_action :add_version_headers
-
end
-
-
private
-
-
# 设置API版本
-
def set_api_version
-
@api_version = ApiVersionService.determine_api_version(request)
-
Thread.current[:api_version] = @api_version
-
end
-
-
# 验证API版本
-
def validate_api_version
-
return if ApiVersionService.version_supported?(@api_version)
-
-
# 版本不支持时返回错误
-
render_error(
-
message: "不支持的API版本: #{@api_version}",
-
error_code: 'unsupported_api_version',
-
details: {
-
requested_version: @api_version,
-
supported_versions: ApiVersionService::SUPPORTED_VERSIONS,
-
recommended_version: ApiVersionService::DEFAULT_VERSION
-
},
-
status_code: 400
-
)
-
end
-
-
# 添加版本相关的响应头
-
def add_version_headers
-
return unless response
-
-
headers = ApiVersionService.create_version_headers(@api_version, response.headers)
-
headers.each do |key, value|
-
response.headers[key] = value
-
end
-
-
# 如果版本已弃用,添加弃用警告到响应中
-
if ApiVersionService.version_deprecated?(@api_version)
-
deprecation_warning = ApiVersionService.generate_deprecation_warning(@api_version)
-
response.headers['X-API-Deprecation-Warning'] = deprecation_warning[:message]
-
end
-
end
-
-
# 获取当前API版本
-
# @return [String] 当前API版本
-
def current_api_version
-
@api_version || ApiVersionService::DEFAULT_VERSION
-
end
-
-
# 检查是否为特定版本
-
# @param version [String] 要检查的版本
-
# @return [Boolean] 是否为指定版本
-
def api_version?(version)
-
current_api_version == version
-
end
-
-
# 检查版本是否为v1
-
# @return [Boolean] 是否为v1
-
def api_v1?
-
api_version?('v1')
-
end
-
-
# 检查版本是否已弃用
-
# @return [Boolean] 是否已弃用
-
def api_version_deprecated?
-
ApiVersionService.version_deprecated?(current_api_version)
-
end
-
-
# 获取版本信息
-
# @return [Hash] 版本信息
-
def current_version_info
-
ApiVersionService.version_info(current_api_version)
-
end
-
-
# 根据版本条件执行代码块
-
# @yield 如果版本匹配,执行给定的代码块
-
# @param version [String] 要匹配的版本
-
def with_api_version(version)
-
yield if api_version?(version)
-
end
-
-
# 版本条件渲染
-
# @param v1_response [Proc] v1版本的响应
-
# @param default_response [Proc] 默认响应
-
def render_by_version(v1_response: nil, default_response: nil)
-
case current_api_version
-
when 'v1'
-
v1_response&.call || default_response&.call
-
else
-
default_response&.call
-
end
-
end
-
-
# 版本化的参数处理
-
# @param params_hash [Hash] 不同版本的参数映射
-
# @return [Hash] 处理后的参数
-
def versioned_params(params_hash = {})
-
case current_api_version
-
when 'v1'
-
params_hash[:v1] || {}
-
else
-
params_hash[:default] || {}
-
end
-
end
-
-
# 版本化的序列化选项
-
# @param options_hash [Hash] 不同版本的选项映射
-
# @return [Hash] 序列化选项
-
def versioned_serialize_options(options_hash = {})
-
base_options = {
-
current_user: current_user,
-
api_version: current_api_version
-
}
-
-
version_options = case current_api_version
-
when 'v1'
-
options_hash[:v1] || {}
-
else
-
options_hash[:default] || {}
-
end
-
-
base_options.merge(version_options)
-
end
-
-
# 版本化的错误处理
-
# @param error_hash [Hash] 不同版本的错误处理映射
-
# @return [Hash] 错误响应
-
def versioned_error_response(error_hash = {})
-
base_error = {
-
api_version: current_api_version,
-
timestamp: Time.current.iso8601
-
}
-
-
version_error = case current_api_version
-
when 'v1'
-
error_hash[:v1] || {}
-
else
-
error_hash[:default] || {}
-
end
-
-
base_error.merge(version_error)
-
end
-
-
# 检查功能是否在当前版本中可用
-
# @param feature [String, Symbol] 功能名称
-
# @return [Boolean] 功能是否可用
-
def feature_available?(feature)
-
case current_api_version
-
when 'v1'
-
available_features = [
-
:user_authentication,
-
:reading_events,
-
:check_ins,
-
:flowers,
-
:comments,
-
:notifications,
-
:analytics,
-
:content_search,
-
:content_export
-
]
-
available_features.include?(feature.to_sym)
-
else
-
false
-
end
-
end
-
-
# 如果功能不可用,返回功能不支持错误
-
# @param feature [String, Symbol] 功能名称
-
# @param message [String] 自定义错误消息
-
def check_feature_availability!(feature, message = nil)
-
return if feature_available?(feature)
-
-
feature_name = feature.to_s.humanize
-
error_message = message || "功能 '#{feature_name}' 在API版本 #{current_api_version} 中不可用"
-
-
render_error(
-
message: error_message,
-
error_code: 'feature_not_available',
-
details: {
-
feature: feature,
-
api_version: current_api_version,
-
available_in_version: find_feature_version(feature)
-
},
-
status_code: 501
-
)
-
end
-
-
private
-
-
# 查找功能可用的版本
-
# @param feature [String, Symbol] 功能名称
-
# @return [String, nil] 可用的版本
-
def find_feature_version(feature)
-
ApiVersionService::SUPPORTED_VERSIONS.find do |version|
-
case version
-
when 'v1'
-
available_features = [
-
:user_authentication,
-
:reading_events,
-
:check_ins,
-
:flowers,
-
:comments,
-
:notifications,
-
:analytics,
-
:content_search,
-
:content_export
-
]
-
available_features.include?(feature.to_sym)
-
end
-
end
-
end
-
end
-
module Authenticable
-
extend ActiveSupport::Concern
-
-
included do
-
before_action :authenticate_user!
-
end
-
-
private
-
-
def authenticate_user!
-
token = extract_token_from_header
-
return render_unauthorized unless token
-
-
decoded = User.decode_jwt_token(token)
-
return render_unauthorized unless decoded
-
-
@current_user = User.find_by(id: decoded[:user_id])
-
render_unauthorized unless @current_user
-
end
-
-
def current_user
-
@current_user
-
end
-
-
def extract_token_from_header
-
header = request.headers["Authorization"]
-
return nil unless header
-
-
# 格式: "Bearer <token>"
-
header.split(" ").last if header.start_with?("Bearer ")
-
end
-
-
def render_unauthorized
-
render json: { error: "Unauthorized" }, status: :unauthorized
-
end
-
end
-
module Commentable
-
extend ActiveSupport::Concern
-
-
included do
-
include ApiResponse
-
before_action :authenticate_user!
-
before_action :set_comment, only: [:update, :destroy]
-
end
-
-
# GET /api/comments
-
def index
-
@comments = fetch_comments.includes(:user).order(created_at: :asc)
-
-
render_success(
-
format_comments_response(@comments),
-
message: '获取评论列表成功'
-
)
-
end
-
-
# POST /api/comments
-
def create
-
@comment = build_comment(comment_params)
-
@comment.user = current_user
-
-
if @comment.save
-
render_created(
-
format_single_comment(@comment, true),
-
message: '评论发布成功'
-
)
-
else
-
render_error(
-
'评论创建失败',
-
errors: @comment.errors.full_messages
-
)
-
end
-
end
-
-
# PUT /api/comments/:id
-
def update
-
unless can_edit_comment?(@comment, current_user)
-
return render_forbidden('无权限编辑此评论')
-
end
-
-
if @comment.update(comment_params)
-
render_success(
-
format_single_comment(@comment, true),
-
message: '评论更新成功'
-
)
-
else
-
render_error(
-
'评论更新失败',
-
errors: @comment.errors.full_messages
-
)
-
end
-
end
-
-
# DELETE /api/comments/:id
-
def destroy
-
unless can_edit_comment?(@comment, current_user)
-
return render_forbidden('无权限删除此评论')
-
end
-
-
@comment.destroy
-
render_success(
-
nil,
-
message: '评论删除成功'
-
)
-
end
-
-
private
-
-
def set_comment
-
@comment = Comment.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render json: { error: '评论不存在' }, status: :not_found
-
end
-
-
def comment_params
-
params.require(:comment).permit(:content)
-
end
-
-
# 抽象方法,由包含的类实现
-
def fetch_comments
-
raise NotImplementedError, "子类必须实现 fetch_comments 方法"
-
end
-
-
def build_comment(params)
-
raise NotImplementedError, "子类必须实现 build_comment 方法"
-
end
-
-
# 格式化评论列表响应
-
def format_comments_response(comments)
-
comments.map { |comment|
-
format_single_comment(comment)
-
}
-
end
-
-
# 格式化单个评论
-
def format_single_comment(comment, can_edit = false)
-
comment.instance_variable_set(:@can_edit_current_user, can_edit || can_edit_comment?(comment, current_user))
-
comment.send(:as_json)
-
end
-
-
# 权限检查 - 使用模型的公共方法
-
def can_edit_comment?(comment, user)
-
comment.send(:can_edit?, user)
-
end
-
end
-
# frozen_string_literal: true
-
-
# GlobalErrorHandler - 全局错误处理模块
-
# 为所有控制器提供统一的错误处理机制
-
module GlobalErrorHandler
-
extend ActiveSupport::Concern
-
-
included do
-
# 全局异常处理
-
rescue_from StandardError, with: :handle_standard_error
-
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found_error
-
rescue_from ActiveRecord::RecordInvalid, with: :handle_validation_error
-
rescue_from ActionController::ParameterMissing, with: :handle_parameter_missing_error
-
rescue_from JWT::DecodeError, with: :handle_jwt_error
-
rescue_from JWT::ExpiredSignature, with: :handle_jwt_expired_error
-
rescue_from ActionController::BadRequest, with: :handle_bad_request_error
-
rescue_from Timeout::Error, with: :handle_timeout_error
-
rescue_from ActiveRecord::StatementInvalid, with: :handle_database_error
-
end
-
-
private
-
-
def handle_standard_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_not_found_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_validation_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_parameter_missing_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_jwt_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_jwt_expired_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_bad_request_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_timeout_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_database_error(exception)
-
handle_error(exception)
-
end
-
-
def handle_error(exception)
-
# 使用全局错误处理服务
-
error_handler = GlobalErrorHandlerService.handle_controller_exception(
-
exception, self, action_name
-
)
-
-
# 记录错误信息
-
log_error(error_handler)
-
-
# 返回错误响应
-
render_error_response(error_handler)
-
end
-
-
def log_error(error_handler)
-
Rails.logger.error(
-
"Controller Error: #{error_handler.error_code}",
-
{
-
controller: self.class.name,
-
action: action_name,
-
user_id: current_user&.id,
-
error_details: error_handler.error_response
-
}
-
)
-
end
-
-
def render_error_response(error_handler)
-
status = determine_http_status(error_handler.error_code)
-
-
render json: error_handler.error_response, status: status
-
end
-
-
def determine_http_status(error_code)
-
case error_code
-
when 'RESOURCE_NOT_FOUND'
-
:not_found
-
when 'VALIDATION_ERROR', 'MISSING_PARAMETER', 'INVALID_PARAMETER'
-
:unprocessable_entity
-
when 'INVALID_TOKEN', 'TOKEN_EXPIRED'
-
:unauthorized
-
when 'TIMEOUT_ERROR', 'DATABASE_ERROR'
-
:service_unavailable
-
else
-
:internal_server_error
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# RequestValidator - 请求验证模块
-
# 提供统一的参数验证和请求安全检查
-
module RequestValidator
-
extend ActiveSupport::Concern
-
-
# 验证必需参数
-
def validate_required_params(*param_names)
-
missing_params = param_names.select { |param| params[param].blank? }
-
-
if missing_params.any?
-
render_parameter_error_response(
-
parameter: missing_params.join(', '),
-
message: "缺少必需的参数: #{missing_params.join(', ')}"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证参数类型
-
def validate_param_type(param_name, expected_type)
-
return true if params[param_name].blank?
-
-
case expected_type.to_s.downcase
-
when 'string'
-
unless params[param_name].is_a?(String)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是字符串类型"
-
)
-
return false
-
end
-
when 'integer'
-
unless params[param_name].to_s.match?(/\A\d+\z/)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是整数类型"
-
)
-
return false
-
end
-
when 'float', 'decimal'
-
unless params[param_name].to_s.match?(/\A\d+(\.\d+)?\z/)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是数字类型"
-
)
-
return false
-
end
-
when 'boolean'
-
unless %w[true false 1 0].include?(params[param_name].to_s.downcase)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是布尔类型"
-
)
-
return false
-
end
-
when 'array'
-
unless params[param_name].is_a?(Array)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是数组类型"
-
)
-
return false
-
end
-
when 'hash', 'object'
-
unless params[param_name].is_a?(Hash) || params[param_name].is_a?(ActionController::Parameters)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是对象类型"
-
)
-
return false
-
end
-
end
-
-
true
-
end
-
-
# 验证参数长度
-
def validate_param_length(param_name, min_length: nil, max_length: nil)
-
value = params[param_name]
-
return true if value.blank?
-
-
length = value.to_s.length
-
-
if min_length && length < min_length
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 长度不能少于 #{min_length} 个字符"
-
)
-
return false
-
end
-
-
if max_length && length > max_length
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 长度不能超过 #{max_length} 个字符"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证数值范围
-
def validate_param_range(param_name, min_value: nil, max_value: nil)
-
value = params[param_name]
-
return true if value.blank?
-
-
numeric_value = value.to_f
-
-
if min_value && numeric_value < min_value
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 不能小于 #{min_value}"
-
)
-
return false
-
end
-
-
if max_value && numeric_value > max_value
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 不能大于 #{max_value}"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证日期格式
-
def validate_date_format(param_name, format: :iso8601)
-
value = params[param_name]
-
return true if value.blank?
-
-
begin
-
case format
-
when :iso8601
-
Date.iso8601(value.to_s)
-
when :date
-
Date.parse(value.to_s)
-
when :datetime
-
DateTime.parse(value.to_s)
-
else
-
Date.parse(value.to_s)
-
end
-
rescue ArgumentError
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 日期格式不正确,请使用 #{format} 格式"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证邮箱格式
-
def validate_email_format(param_name)
-
value = params[param_name]
-
return true if value.blank?
-
-
email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
-
-
unless value.match?(email_regex)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 邮箱格式不正确"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证URL格式
-
def validate_url_format(param_name)
-
value = params[param_name]
-
return true if value.blank?
-
-
uri = URI.parse(value)
-
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} URL格式不正确"
-
)
-
return false
-
end
-
-
true
-
rescue URI::InvalidURIError
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} URL格式不正确"
-
)
-
false
-
end
-
-
# 验证枚举值
-
def validate_enum_values(param_name, allowed_values)
-
value = params[param_name]
-
return true if value.blank?
-
-
unless allowed_values.include?(value)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是以下值之一: #{allowed_values.join(', ')}"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证分页参数
-
def validate_pagination_params
-
# 验证页码
-
if params[:page].present?
-
unless validate_param_type(:page, 'integer')
-
return false
-
end
-
-
unless validate_param_range(:page, min_value: 1)
-
return false
-
end
-
end
-
-
# 验证每页数量
-
if params[:per_page].present?
-
unless validate_param_type(:per_page, 'integer')
-
return false
-
end
-
-
unless validate_param_range(:per_page, min_value: 1, max_value: 100)
-
return false
-
end
-
end
-
-
# 设置默认值
-
params[:page] ||= 1
-
params[:per_page] ||= 20
-
-
true
-
end
-
-
# 验证排序参数
-
def validate_sort_params(allowed_fields = nil)
-
if params[:sort_by].present?
-
# 验证排序字段
-
if allowed_fields && !allowed_values.include?(params[:sort_by])
-
render_parameter_error_response(
-
parameter: 'sort_by',
-
message: "排序字段必须是以下值之一: #{allowed_fields.join(', ')}"
-
)
-
return false
-
end
-
-
# 验证排序方向
-
if params[:sort_direction].present?
-
unless validate_enum_values(:sort_direction, ['asc', 'desc'])
-
return false
-
end
-
else
-
params[:sort_direction] = 'desc'
-
end
-
end
-
-
true
-
end
-
-
# 验证文件上传
-
def validate_file_upload(param_name, max_size: nil, allowed_types: nil)
-
file = params[param_name]
-
return true if file.blank?
-
-
# 验证文件大小
-
if max_size && file.size > max_size
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "文件大小不能超过 #{max_size / 1024 / 1024}MB"
-
)
-
return false
-
end
-
-
# 验证文件类型
-
if allowed_types && !allowed_types.include?(file.content_type)
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "文件类型必须是以下类型之一: #{allowed_types.join(', ')}"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证批量操作参数
-
def validate_batch_operation_params
-
unless validate_required_params(:ids)
-
return false
-
end
-
-
unless validate_param_type(:ids, 'array')
-
return false
-
end
-
-
if params[:ids].length > 100
-
render_parameter_error_response(
-
parameter: 'ids',
-
message: "批量操作最多支持100个项目"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证JSON格式
-
def validate_json_param(param_name)
-
value = params[param_name]
-
return true if value.blank?
-
-
begin
-
JSON.parse(value.to_s)
-
rescue JSON::ParserError
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "参数 #{param_name} 必须是有效的JSON格式"
-
)
-
return false
-
end
-
-
true
-
end
-
-
# 验证时间范围
-
def validate_time_range(start_param, end_param)
-
if params[start_param].present? && params[end_param].present?
-
unless validate_date_format(start_param) && validate_date_format(end_param)
-
return false
-
end
-
-
start_time = params[start_param].to_s
-
end_time = params[end_param].to_s
-
-
if Time.parse(end_time) < Time.parse(start_time)
-
render_parameter_error_response(
-
parameter: end_param,
-
message: "结束时间不能早于开始时间"
-
)
-
return false
-
end
-
end
-
-
true
-
end
-
-
# 综合验证方法
-
def validate_request_params(validations = {})
-
validations.each do |param_name, options|
-
# 验证必需参数
-
if options[:required] && params[param_name].blank?
-
render_parameter_error_response(
-
parameter: param_name,
-
message: "缺少必需的参数: #{param_name}"
-
)
-
return false
-
end
-
-
# 跳过空值的后续验证
-
next if params[param_name].blank?
-
-
# 验证类型
-
if options[:type] && !validate_param_type(param_name, options[:type])
-
return false
-
end
-
-
# 验证长度
-
if options[:length] && !validate_param_length(param_name, **options[:length])
-
return false
-
end
-
-
# 验证范围
-
if options[:range] && !validate_param_range(param_name, **options[:range])
-
return false
-
end
-
-
# 验证格式
-
if options[:format] == :email && !validate_email_format(param_name)
-
return false
-
elsif options[:format] == :url && !validate_url_format(param_name)
-
return false
-
elsif options[:format] == :json && !validate_json_param(param_name)
-
return false
-
end
-
-
# 验证枚举值
-
if options[:in] && !validate_enum_values(param_name, options[:in])
-
return false
-
end
-
end
-
-
true
-
end
-
end
-
# frozen_string_literal: true
-
-
# UserExperienceEnhancer - 用户体验增强模块
-
# 为控制器添加用户体验增强功能
-
module UserExperienceEnhancer
-
extend ActiveSupport::Concern
-
-
# 在响应中添加用户体验增强数据
-
def enhance_response_with_user_experience(response_data = nil)
-
return response_data unless current_user
-
-
enhanced_data = response_data || {}
-
user_experience_data = build_user_experience_data
-
-
# 将用户体验数据添加到响应中
-
if enhanced_data.is_a?(Hash)
-
enhanced_data[:user_experience] = user_experience_data
-
else
-
# 如果响应数据不是Hash,包装它
-
enhanced_data = {
-
data: enhanced_data,
-
user_experience: user_experience_data
-
}
-
end
-
-
enhanced_data
-
end
-
-
private
-
-
def build_user_experience_data
-
enhancer_service = UserExperienceEnhancerService.new(
-
user: current_user,
-
request_context: build_request_context,
-
enhancement_options: enhancement_options
-
)
-
-
enhancer_service.call
-
-
{
-
recommendations: enhancer_service.recommendations,
-
personalization: enhancer_service.personalization_data,
-
quick_actions: enhancer_service.send(:generate_quick_actions),
-
contextual_tips: enhancer_service.send(:generate_contextual_tips),
-
preferences: build_preferences_data,
-
notifications: build_notifications_data
-
}
-
end
-
-
def build_request_context
-
{
-
action: action_name,
-
controller: controller_name,
-
current_page: detect_current_page,
-
user_agent: request.user_agent,
-
timestamp: Time.current,
-
parameters: filtered_parameters
-
}
-
end
-
-
def enhancement_options
-
{
-
include_recommendations: should_include_recommendations?,
-
include_personalization: should_include_personalization?,
-
include_quick_actions: should_include_quick_actions?,
-
include_tips: should_include_tips?
-
}
-
end
-
-
def should_include_recommendations?
-
# 在主页、列表页面显示推荐
-
%w[index show].include?(action_name) && !request.format.json?
-
end
-
-
def should_include_personalization?
-
# 为认证用户显示个性化内容
-
current_user.present?
-
end
-
-
def should_include_quick_actions?
-
# 在所有页面显示快捷操作
-
current_user.present?
-
end
-
-
def should_include_tips?
-
# 根据时间和用户行为显示提示
-
contextual_tips_enabled?
-
end
-
-
def contextual_tips_enabled?
-
# 可以基于用户设置或系统配置
-
current_user&.preferences&.dig('contextual_tips_enabled') != false
-
end
-
-
def detect_current_page
-
case controller_name
-
when 'posts'
-
'posts'
-
when 'reading_events'
-
'events'
-
when 'users'
-
'profile'
-
when 'notifications'
-
'notifications'
-
else
-
'other'
-
end
-
end
-
-
def build_preferences_data
-
return {} unless current_user
-
-
{
-
theme: current_user.preferences&.dig('theme') || 'light',
-
language: current_user.preferences&.dig('language') || 'zh-CN',
-
notifications_enabled: current_user.preferences&.dig('notifications_enabled') != false,
-
auto_refresh: current_user.preferences&.dig('auto_refresh') || false
-
}
-
end
-
-
def build_notifications_data
-
return {} unless current_user
-
-
unread_count = current_user.notifications.where(read: false).count
-
recent_notifications = current_user.notifications
-
.order(created_at: :desc)
-
.limit(5)
-
-
{
-
unread_count: unread_count,
-
recent_count: recent_notifications.count,
-
has_new_notifications: unread_count > 0,
-
notification_types: get_notification_types(recent_notifications)
-
}
-
end
-
-
def get_notification_types(notifications)
-
types = notifications.pluck(:notification_type)
-
types.group_by(&:itself).transform_values(&:count)
-
end
-
-
def filtered_parameters
-
# 过滤敏感参数
-
allowed_params = %w[page per_page sort_by sort_direction category status]
-
params.to_h.select { |key, _| allowed_params.include?(key.to_s) }
-
end
-
-
# 重写渲染方法以自动添加用户体验增强
-
def render_success_response(data: nil, message: 'Success', meta: {})
-
enhanced_data = enhance_response_with_user_experience(data)
-
super(data: enhanced_data, message: message, meta: meta)
-
end
-
-
def render_paginated_response(data:, pagination:, message: 'Success', meta: {})
-
enhanced_data = enhance_response_with_user_experience(data)
-
super(data: enhanced_data, pagination: pagination, message: message, meta: meta)
-
end
-
-
# 渲染增强的用户体验响应
-
def render_enhanced_response(data: nil, message: 'Success', status: :ok)
-
enhanced_response = {
-
success: true,
-
message: message,
-
data: enhance_response_with_user_experience(data),
-
timestamp: Time.current.iso8601,
-
request_id: @request_id
-
}
-
-
render json: enhanced_response, status: status
-
end
-
-
# 添加用户行为追踪
-
def track_user_action(action_type, details = {})
-
return unless current_user
-
-
UserActivityTracker.track(
-
user: current_user,
-
action_type: action_type,
-
details: details.merge(
-
controller: controller_name,
-
action: action_name,
-
ip: request.remote_ip,
-
user_agent: request.user_agent
-
)
-
)
-
end
-
-
# 记录用户偏好
-
def record_user_preference(preference_key, value)
-
return unless current_user
-
-
preferences = current_user.preferences || {}
-
preferences[preference_key] = value
-
-
current_user.update(preferences: preferences)
-
end
-
-
# 获取用户最近活动
-
def get_recent_user_activities(limit = 5)
-
return [] unless current_user
-
-
UserActivity.where(user: current_user)
-
.order(created_at: :desc)
-
.limit(limit)
-
end
-
-
# 检查用户是否为新用户
-
def new_user?
-
return false unless current_user
-
current_user.created_at > 7.days.ago
-
end
-
-
# 检查用户是否需要引导
-
def needs_onboarding?
-
return false unless current_user
-
-
# 新用户且完成度低
-
new_user? && user_completion_percentage < 50
-
end
-
-
def user_completion_percentage
-
return 0 unless current_user
-
-
completion_items = [
-
current_user.nickname.present?,
-
current_user.avatar.present?,
-
current_user.posts.count > 0,
-
current_user.comments.count > 0,
-
current_user.event_enrollments.count > 0
-
]
-
-
(completion_items.count(true) * 100 / completion_items.length).round
-
end
-
-
# 检查用户是否需要鼓励
-
def needs_encouragement?
-
return false unless current_user
-
-
# 长时间未活跃的用户
-
last_activity = current_user.posts.maximum(:created_at) || current_user.created_at
-
last_activity < 7.days.ago
-
end
-
-
# 生成鼓励消息
-
def generate_encouragement_message
-
return nil unless needs_encouragement?
-
-
messages = [
-
"好久不见,想念您的分享!",
-
"新的精彩内容等您发现",
-
"朋友们都很想念您的参与",
-
"分享您的读书心得吧"
-
]
-
-
messages.sample
-
end
-
-
# 添加个性化响应头
-
def add_personalization_headers
-
return unless current_user
-
-
response.headers['X-User-Level'] = calculate_user_level.to_s
-
response.headers['X-New-User'] = new_user?.to_s
-
response.headers['X-Needs-Onboarding'] = needs_onboarding?.to_s
-
response.headers['X-User-Timezone'] = current_user.preferences&.dig('timezone') || 'Asia/Shanghai'
-
end
-
-
def calculate_user_level
-
# 简化的用户等级计算
-
score = 0
-
score += (current_user.posts.count * 10)
-
score += (current_user.comments.count * 5)
-
score += (current_user.event_enrollments.count * 15)
-
-
case score
-
when 0..50
-
1
-
when 51..200
-
2
-
when 201..500
-
3
-
when 501..1000
-
4
-
else
-
5
-
end
-
end
-
-
# 检查并设置用户偏好
-
def set_user_preferences_if_needed
-
return unless current_user
-
-
# 如果用户没有偏好设置,设置默认值
-
if current_user.preferences.blank?
-
default_preferences = {
-
'theme' => 'light',
-
'language' => 'zh-CN',
-
'timezone' => 'Asia/Shanghai',
-
'notifications_enabled' => true,
-
'contextual_tips_enabled' => true,
-
'auto_refresh' => false
-
}
-
-
current_user.update(preferences: default_preferences)
-
end
-
end
-
-
# 在每个请求开始时调用
-
def enhance_user_request
-
return unless current_user
-
-
# 设置用户偏好
-
set_user_preferences_if_needed
-
-
# 添加个性化响应头
-
add_personalization_headers
-
-
# 追踪用户活动
-
track_user_action("page_view", {
-
path: request.path,
-
method: request.method
-
})
-
end
-
end
-
class ApplicationJob < ActiveJob::Base
-
# Automatically retry jobs that encountered a deadlock
-
# retry_on ActiveRecord::Deadlocked
-
-
# Most jobs are safe to ignore if the underlying records are no longer available
-
# discard_on ActiveJob::DeserializationError
-
end
-
class ApplicationMailer < ActionMailer::Base
-
default from: "from@example.com"
-
layout "mailer"
-
end
-
1
class ApplicationRecord < ActiveRecord::Base
-
1
primary_abstract_class
-
end
-
# == Schema Information
-
#
-
# Table name: check_ins
-
#
-
# id :integer not null, primary key
-
# user_id :integer not null
-
# reading_schedule_id :integer not null
-
# enrollment_id :integer not null
-
# content :text not null
-
# word_count :integer default(0), not null
-
# status :integer default(0), not null
-
# submitted_at :datetime not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# index_check_ins_on_reading_schedule_id (reading_schedule_id)
-
# index_check_ins_on_submitted_at (submitted_at)
-
# index_check_ins_on_user_id (user_id)
-
# index_check_ins_on_user_id_and_reading_schedule_id (user_id, reading_schedule_id) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (enrollment_id => event_enrollments.id)
-
# fk_rails_... (reading_schedule_id => reading_schedules.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
-
1
class CheckIn < ApplicationRecord
-
# 打卡状态枚举
-
1
enum :status, {
-
normal: 0, # 正常打卡
-
supplement: 1, # 补卡
-
late: 2 # 迟到
-
}, default: :normal
-
-
# 关联关系
-
1
belongs_to :user
-
1
belongs_to :reading_schedule
-
1
belongs_to :enrollment, class_name: 'EventEnrollment'
-
1
has_many :flowers, dependent: :destroy
-
1
has_one :reading_event, through: :reading_schedule
-
1
has_many :comments, as: :commentable, dependent: :destroy
-
-
# 验证规则
-
1
validates :content, presence: true, length: { minimum: 50, maximum: 2000 }
-
1
validates :word_count, numericality: { greater_than_or_equal_to: 0 }
-
1
validates :user_id, uniqueness: { scope: :reading_schedule_id, message: "今日已打卡" }
-
1
validate :must_be_active_participant
-
1
validate :schedule_within_activity_period
-
1
validate :content_word_count_limit
-
1
validate :cannot_check_in_after_deadline, on: :create
-
-
# 回调
-
1
before_validation :calculate_word_count, if: :content_changed?
-
1
before_validation :set_status, on: :create
-
1
before_create :set_submitted_at
-
1
after_create :update_enrollment_stats
-
1
after_create :notify_check_in_submitted
-
1
after_destroy :rollback_enrollment_stats
-
-
# 作用域
-
1
scope :today, -> { joins(:reading_schedule).where(reading_schedules: { date: Date.current }) }
-
1
scope :normal, -> { where(status: :normal) }
-
1
scope :supplement, -> { where(status: :supplement) }
-
1
scope :late, -> { where(status: :late) }
-
1
scope :recent, -> { order(submitted_at: :desc) }
-
1
scope :by_word_count, ->(direction = :desc) { order(word_count: direction) }
-
-
# 委托方法
-
1
delegate :title, to: :reading_event, prefix: true
-
1
delegate :nickname, to: :user, prefix: true
-
1
delegate :date, to: :reading_schedule, prefix: true
-
-
# 状态方法
-
1
def today?
-
reading_schedule.today?
-
end
-
-
1
def on_time?
-
return true if status == 'normal'
-
false
-
end
-
-
1
def is_supplement?
-
status == 'supplement'
-
end
-
-
1
def is_late?
-
status == 'late'
-
end
-
-
1
def can_be_edited?
-
# 活动结束后不能编辑
-
return false unless reading_event
-
reading_event.end_date >= Date.current
-
end
-
-
1
def can_receive_flowers?
-
flowers_count < 3 # 每个打卡最多3朵小红花
-
end
-
-
1
def can_be_deleted?
-
# 活动结束后不能删除
-
return false unless reading_event
-
reading_event.end_date >= Date.current
-
end
-
-
# 统计方法
-
1
def flowers_count
-
flowers.count
-
end
-
-
1
def total_flowers_received
-
flowers.sum(&:amount) if flowers.respond_to?(:sum)
-
end
-
-
1
def engagement_score
-
# 计算参与度分数:字数分数 + 小红花分数
-
word_score = [word_count / 100.0, 10.0].min # 最多10分
-
flower_score = flowers_count * 2.0 # 每朵小红花2分
-
(word_score + flower_score).round(2)
-
end
-
-
# 小红花相关方法
-
1
def give_flower!(giver, comment = nil)
-
return false unless can_receive_flowers?
-
return false if giver == user # 不能给自己发小红花
-
-
# 检查发放权限
-
unless reading_event.can_give_flowers?(giver, reading_schedule)
-
return false
-
end
-
-
transaction do
-
flower = flowers.create!(
-
giver: giver,
-
recipient: user,
-
comment: comment,
-
reading_schedule: reading_schedule
-
)
-
-
# 更新接收者的统计
-
enrollment.increment!(:flowers_received_count)
-
-
# 发送通知
-
notify_flower_given(flower)
-
end
-
true
-
end
-
-
# 内容方法
-
1
def content_preview(length = 100)
-
content.truncate(length)
-
end
-
-
1
def reading_time_estimate
-
# 基于字数估算阅读时间(假设每分钟200字)
-
(word_count / 200.0).ceil
-
end
-
-
# 格式化内容
-
1
def formatted_content(options = {})
-
ContentFormatterService.format(content, options)
-
end
-
-
# 内容摘要
-
1
def content_summary(max_length = 200)
-
ContentFormatterService.generate_summary(content, max_length)
-
end
-
-
# 提取关键词
-
1
def keywords(max_count = 5)
-
ContentFormatterService.extract_keywords(content, max_count)
-
end
-
-
# 内容质量分数
-
1
def quality_score
-
ContentFormatterService.calculate_quality_score(content)
-
end
-
-
# 内容合规性检查
-
1
def compliance_check
-
ContentFormatterService.check_compliance(content)
-
end
-
-
# 是否为高质量内容
-
1
def high_quality?
-
quality_score >= 50
-
end
-
-
# 是否有格式问题
-
1
def has_formatting_issues?
-
check = compliance_check
-
check[:issues].any? { |issue| issue[:type] == 'poor_formatting' }
-
end
-
-
# 是否包含敏感词
-
1
def contains_sensitive_words?
-
check = compliance_check
-
check[:issues].any? { |issue| issue[:type] == 'sensitive_words' }
-
end
-
-
# API响应格式化
-
1
def as_json_for_api(options = {})
-
base_data = {
-
id: id,
-
content: content,
-
word_count: word_count,
-
status: status,
-
submitted_at: submitted_at,
-
created_at: created_at,
-
updated_at: updated_at,
-
engagement_score: engagement_score,
-
quality_score: quality_score,
-
high_quality: high_quality?
-
}
-
-
# 可选包含关联数据
-
if options[:include_user]
-
base_data[:user] = user.as_json_for_api
-
end
-
-
if options[:include_reading_schedule]
-
base_data[:reading_schedule] = {
-
id: reading_schedule.id,
-
day_number: reading_schedule.day_number,
-
date: reading_schedule.date,
-
reading_progress: reading_schedule.reading_progress
-
}
-
end
-
-
if options[:include_reading_event]
-
base_data[:reading_event] = {
-
id: reading_event.id,
-
title: reading_event.title,
-
book_name: reading_event.book_name
-
}
-
end
-
-
if options[:include_flowers]
-
base_data[:flowers] = flowers.map do |flower|
-
{
-
id: flower.id,
-
amount: flower.amount,
-
flower_type: flower.flower_type,
-
comment: flower.comment,
-
giver: flower.giver.as_json_for_api,
-
created_at: flower.created_at
-
}
-
end
-
base_data[:flowers_count] = flowers_count
-
base_data[:total_flowers_received] = total_flowers_received
-
end
-
-
if options[:include_comments]
-
base_data[:comments] = comments.map(&:as_json_for_api)
-
base_data[:comments_count] = comments.count
-
end
-
-
# 内容相关
-
if options[:include_content_analysis]
-
base_data[:content_preview] = content_preview(options[:preview_length] || 100)
-
base_data[:reading_time_estimate] = reading_time_estimate
-
base_data[:keywords] = keywords
-
base_data[:content_summary] = content_summary
-
end
-
-
base_data
-
end
-
-
1
private
-
-
# 验证方法
-
1
def must_be_active_participant
-
return unless enrollment && reading_event
-
-
# 直接检查报名状态和类型,避免调用私有方法
-
unless enrollment.enrolled? && enrollment.participant?
-
errors.add(:base, "您不是该活动的有效参与者")
-
end
-
end
-
-
1
def schedule_within_activity_period
-
return unless reading_schedule && reading_event
-
-
schedule_date = reading_schedule.date
-
unless schedule_date.between?(reading_event.start_date, reading_event.end_date)
-
errors.add(:base, "打卡日期不在活动期间内")
-
end
-
end
-
-
1
def content_word_count_limit
-
return unless content
-
-
if word_count < 50
-
errors.add(:content, "内容太短,至少需要50个字")
-
elsif word_count > 2000
-
errors.add(:content, "内容太长,最多2000个字")
-
end
-
end
-
-
1
def cannot_check_in_after_deadline
-
return unless reading_schedule
-
-
# 当天的打卡可以在晚上11:59前提交
-
schedule_date = reading_schedule.date
-
deadline = schedule_date.to_time.end_of_day
-
-
if Time.current > deadline && status == 'normal'
-
errors.add(:base, "打卡时间已过,只能补卡")
-
end
-
end
-
-
# 回调方法
-
1
def calculate_word_count
-
self.word_count = content.to_s.strip.length
-
end
-
-
1
def set_status
-
schedule_date = reading_schedule.date
-
current_time = Time.current
-
-
if schedule_date == Date.current
-
self.status = 'normal'
-
elsif schedule_date < Date.current
-
self.status = 'supplement'
-
elsif current_time > schedule_date.to_time.end_of_day
-
self.status = 'late'
-
end
-
end
-
-
1
def set_submitted_at
-
self.submitted_at ||= Time.current
-
end
-
-
1
def update_enrollment_stats
-
return unless enrollment
-
-
enrollment.increment!(:check_ins_count)
-
enrollment.update_completion_rate!
-
end
-
-
1
def rollback_enrollment_stats
-
return unless enrollment
-
-
# 减少打卡次数
-
enrollment.decrement!(:check_ins_count)
-
-
# 减少小红花数量(如果有)
-
flowers_count = flowers.count
-
if flowers_count > 0
-
enrollment.decrement!(:flowers_received_count, flowers_count)
-
end
-
-
# 重新计算完成率
-
enrollment.update_completion_rate!
-
end
-
-
# 通知方法
-
1
def notify_check_in_submitted
-
# 发送打卡提交通知
-
CheckInNotificationService.notify_submitted(self)
-
end
-
-
1
def notify_flower_given(flower)
-
# 发送小红花通知
-
FlowerNotificationService.notify_given(flower)
-
end
-
-
# 是否获得小红花
-
1
def has_flower?
-
flower.present?
-
end
-
-
# 是否可以补卡
-
1
def can_makeup?
-
reading_schedule.date < Date.today &&
-
reading_event.in_progress?
-
end
-
end
-
1
class Comment < ApplicationRecord
-
1
belongs_to :post, optional: true
-
1
belongs_to :user
-
1
belongs_to :commentable, polymorphic: true
-
-
# 验证
-
1
validates :content, presence: true, length: { minimum: 2, maximum: 1000 }
-
1
validates :commentable, presence: true, if: :should_validate_commentable?
-
-
1
private
-
-
1
def should_validate_commentable?
-
34
commentable_type != 'CheckIn'
-
end
-
-
# 权限检查方法 - 改为公共方法
-
1
def can_edit?(current_user)
-
return false unless current_user
-
return true if current_user.any_admin? # 管理员可以编辑任何评论
-
return true if user_id == current_user.id # 作者可以编辑自己的评论
-
false
-
end
-
-
# 时间格式化
-
1
def time_ago
-
seconds = Time.current - created_at
-
minutes = seconds / 60
-
hours = minutes / 60
-
days = hours / 24
-
-
if days >= 1
-
"#{days.to_i}天前"
-
elsif hours >= 1
-
"#{hours.to_i}小时前"
-
elsif minutes >= 1
-
"#{minutes.to_i}分钟前"
-
else
-
"刚刚"
-
end
-
end
-
-
# API序列化方法 - 标准化API响应格式
-
1
def as_json_for_api(options = {})
-
current_user = options[:current_user]
-
-
result = {
-
id: id,
-
content: content,
-
created_at: created_at,
-
updated_at: updated_at,
-
time_ago: time_ago,
-
author: user.as_json_for_api
-
}
-
-
# 添加评论对象信息
-
if commentable
-
result[:commentable] = {
-
type: commentable_type,
-
id: commentable_id,
-
title: commentable_title
-
}
-
end
-
-
# 添加当前用户的权限信息
-
if current_user
-
result[:interactions] = {
-
can_edit: can_edit?(current_user)
-
}
-
end
-
-
# 包含回复评论
-
if options[:include_replies]
-
result[:replies] = replies.limit(5).map { |reply| reply.as_json_for_api(options) }
-
end
-
-
result
-
end
-
-
# JSON 序列化方法 - 保持向后兼容
-
1
def as_json(options = {})
-
json_hash = {
-
id: id,
-
content: content,
-
created_at: created_at,
-
updated_at: updated_at,
-
author_info: author_info,
-
time_ago: time_ago,
-
can_edit_current_user: @can_edit_current_user || false
-
}
-
-
# 如果有关联的用户信息,包含用户数据
-
if associated_user_loaded?
-
json_hash[:user] = {
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
}
-
end
-
-
json_hash
-
end
-
-
# 设置当前用户是否可编辑的权限 - 改为公共方法
-
1
def can_edit_current_user=(value)
-
@can_edit_current_user = value
-
end
-
-
# 检查用户数据是否已预加载 - 改为公共方法
-
1
def associated_user_loaded?
-
loaded_associations = association(:user).loaded?
-
loaded_associations
-
rescue
-
false
-
end
-
-
# 获取评论对象的标题
-
1
def commentable_title
-
return unless commentable
-
-
case commentable_type
-
when 'Post'
-
commentable.title
-
when 'CheckIn'
-
"第#{commentable.day_number}天打卡"
-
when 'ReadingEvent'
-
commentable.title
-
when 'Flower'
-
"小红花 #{commentable.id}"
-
else
-
commentable_type
-
end
-
end
-
-
# 获取回复评论
-
1
def replies
-
Comment.where(commentable_type: 'Comment', commentable_id: id)
-
end
-
-
1
private
-
-
1
def author_info
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url,
-
role: user.role_display_name
-
}
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: content_reports
-
#
-
# id :integer not null, primary key
-
# user_id :integer not null, foreign_key
-
# check_in_id :integer not null, foreign_key
-
# admin_id :integer foreign_key
-
# reason :enum default("other"), not null
-
# description :text
-
# status :enum default("pending"), not null
-
# admin_notes :text
-
# reviewed_at :datetime
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# index_content_reports_on_check_in_id (check_in_id)
-
# index_content_reports_on_created_at (created_at)
-
# index_content_reports_on_reason (reason)
-
# index_content_reports_on_status (status)
-
# index_content_reports_on_user_id (user_id)
-
# index_content_reports_unique_reporting (user_id, check_in_id) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (check_in_id => check_ins.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
-
class ContentReport < ApplicationRecord
-
# 举报原因枚举
-
enum :reason, {
-
sensitive_words: '敏感词',
-
inappropriate_content: '不当内容',
-
spam: '垃圾内容',
-
other: '其他'
-
}, default: :other
-
-
# 处理状态枚举
-
enum :status, {
-
pending: '待处理',
-
reviewed: '已查看',
-
dismissed: '已忽略',
-
action_taken: '已处理'
-
}, default: :pending
-
-
# 关联关系
-
belongs_to :user
-
belongs_to :check_in
-
belongs_to :admin, class_name: 'User', optional: true
-
-
# 验证规则
-
validates :user_id, uniqueness: { scope: :check_in_id, message: '您已经举报过此内容' }
-
validates :description, length: { maximum: 500 }
-
validate :cannot_report_own_content
-
-
# 回调
-
after_create :notify_admins
-
after_update :send_status_update_notification
-
-
# 作用域
-
scope :pending, -> { where(status: :pending) }
-
scope :reviewed, -> { where.not(status: :pending) }
-
scope :by_reason, ->(reason) { where(reason: reason) }
-
scope :recent, -> { order(created_at: :desc) }
-
-
# 委托方法
-
delegate :content, to: :check_in, prefix: true
-
delegate :nickname, to: :user, prefix: true
-
delegate :created_at, to: :check_in, prefix: true
-
-
# 状态方法
-
def pending?
-
status == 'pending'
-
end
-
-
def reviewed?
-
reviewed_at.present?
-
end
-
-
def processed?
-
%w[reviewed dismissed action_taken].include?(status.to_s)
-
end
-
-
def action_taken?
-
status == 'action_taken'
-
end
-
-
# 操作方法
-
def review!(admin:, notes: nil, action: :reviewed)
-
return false unless admin.can_approve_events?
-
-
transaction do
-
update!(
-
admin: admin,
-
admin_notes: notes,
-
status: action,
-
reviewed_at: Time.current
-
)
-
-
# 根据处理结果执行相应操作
-
case action.to_sym
-
when :action_taken
-
handle_content_action
-
end
-
-
log_review_action(admin, action)
-
end
-
-
true
-
rescue => e
-
Rails.logger.error "Content report review failed: #{e.message}"
-
false
-
end
-
-
def dismiss!(admin:, notes: nil)
-
review!(admin: admin, notes: notes, action: :dismissed)
-
end
-
-
# 类方法
-
def self.statistics(days = 30)
-
start_date = days.days.ago.to_date
-
-
{
-
total_reports: where('created_at >= ?', start_date).count,
-
pending_reports: pending.where('created_at >= ?', start_date).count,
-
by_reason: where('created_at >= ?', start_date).group(:reason).count,
-
by_status: where('created_at >= ?', start_date).group(:status).count,
-
daily_trends: where('created_at >= ?', start_date)
-
.group('DATE(created_at)')
-
.count
-
}
-
end
-
-
def self.high_priority_reports
-
# 需要优先处理的举报
-
pending.joins(:check_in)
-
.where('check_ins.created_at < ?', 1.hour.ago)
-
.or(where(reason: :sensitive_words))
-
end
-
-
private
-
-
# 验证方法
-
def cannot_report_own_content
-
if user_id == check_in.user_id
-
errors.add(:base, '不能举报自己的内容')
-
end
-
end
-
-
# 回调方法
-
def notify_admins
-
# 通知管理员有新的举报
-
return unless Rails.env.production?
-
-
# 这里可以实现邮件、短信或推送通知
-
ContentModerationService.notify_admins_of_new_report(self)
-
end
-
-
def send_status_update_notification
-
# 向举报人发送状态更新通知
-
return unless saved_change_to_status?
-
-
ContentModerationService.notify_reporter_of_status_change(self)
-
end
-
-
def handle_content_action
-
# 处理内容(如隐藏、删除等)
-
case reason.to_sym
-
when :sensitive_words
-
# 可以隐藏包含敏感词的内容
-
check_in.update!(status: :hidden) if check_in.respond_to?(:status=)
-
when :spam
-
# 可以删除垃圾内容
-
check_in.destroy
-
end
-
end
-
-
def log_review_action(admin, action)
-
# 记录管理员操作日志
-
Rails.logger.info "ContentReport##{id} reviewed by #{admin.nickname} with action: #{action}"
-
end
-
end
-
class DailyFlowerStat < ApplicationRecord
-
# 关联
-
belongs_to :reading_event
-
-
# 验证
-
validates :reading_event_id, :stats_date, :leaderboard_data, :generated_at, presence: true
-
validates :stats_date, uniqueness: { scope: :reading_event_id }
-
-
# 作用域
-
scope :for_event, ->(event) { where(reading_event: event) }
-
scope :for_date, ->(date) { where(stats_date: date) }
-
scope :recent_first, -> { order(generated_at: :desc) }
-
scope :generated_between, ->(start_date, end_date) { where(generated_at: start_date..end_date) }
-
-
# 回调
-
before_validation :set_generated_at, on: :create
-
-
# 实例方法
-
-
# 获取排行榜数据(解析JSON)
-
def leaderboard
-
return [] unless leaderboard_data.is_a?(Hash)
-
-
leaderboard_data['rankings'] || []
-
end
-
-
# 获取前三名
-
def top_three
-
leaderboard.first(3)
-
end
-
-
# 获取指定用户的排名
-
def user_ranking(user)
-
return nil unless user
-
-
leaderboard.find { |entry| entry['user_id'] == user.id }
-
end
-
-
# 获取分享文案
-
def share_text_for_wechat
-
return share_text if share_text.present?
-
-
default_text = "🌸 #{reading_event.title} #{stats_date.strftime('%m月%d日')}小红花排行榜\n\n"
-
default_text += "🏆 今日小红花TOP3:\n"
-
-
top_three.each_with_index do |entry, index|
-
user = User.find_by(id: entry['user_id'])
-
next unless user
-
-
emoji = ['🥇', '🥈', '🥉'][index]
-
default_text += "#{emoji} #{user.nickname} - #{entry['total_flowers']}朵\n"
-
end
-
-
default_text += "\n💝 总计#{total_flowers_given}朵小红花,#{total_participants}位小伙伴参与"
-
default_text
-
end
-
-
# 检查是否为今日统计
-
def for_today?
-
stats_date == Date.current
-
end
-
-
# 检查是否为昨日统计
-
def for_yesterday?
-
stats_date == Date.yesterday
-
end
-
-
# 增加分享次数
-
def increment_share_count!
-
increment!(:share_count)
-
end
-
-
# 生成分享图片URL(占位符,实际实现需要集成图片生成服务)
-
def generate_share_image_url
-
# 这里可以集成第三方图片生成服务,如:
-
# - 使用Canvas API生成图片
-
# - 使用微信小程序生成分享图片
-
# - 使用第三方API服务
-
-
timestamp = generated_at.to_i
-
"https://api.example.com/share-images/daily-flower-stats/#{id}?t=#{timestamp}"
-
end
-
-
# API响应格式
-
def as_json_for_api
-
{
-
id: id,
-
reading_event: reading_event.as_json_for_api,
-
stats_date: stats_date,
-
leaderboard: leaderboard,
-
top_three: top_three.map do |entry|
-
user = User.find_by(id: entry['user_id'])
-
{
-
rank: entry['rank'],
-
user: user&.as_json_for_api,
-
total_flowers: entry['total_flowers'],
-
flowers_received: entry['flowers_received'],
-
flowers_given: entry['flowers_given']
-
}
-
end,
-
statistics: {
-
total_flowers_given: total_flowers_given,
-
total_participants: total_participants,
-
total_givers: total_givers,
-
share_count: share_count
-
},
-
share_info: {
-
image_url: share_image_url || generate_share_image_url,
-
text: share_text_for_wechat,
-
share_count: share_count
-
},
-
generated_at: generated_at,
-
for_today: for_today?,
-
for_yesterday: for_yesterday?
-
}
-
end
-
-
# 类方法
-
-
# 获取或创建指定日期的统计
-
def self.get_or_create_daily_stat(event, date = Date.yesterday)
-
find_or_create_by(reading_event: event, stats_date: date) do |stat|
-
stat.generated_at = Time.current
-
stat.generated_by = 'system_auto'
-
end
-
end
-
-
# 检查是否已存在指定日期的统计
-
def self.exists_for_date?(event, date)
-
exists_by?(reading_event: event, stats_date: date)
-
end
-
-
# 获取活动的统计历史
-
def self.event_statistics_history(event, limit: 30)
-
for_event(event)
-
.recent_first
-
.limit(limit)
-
end
-
-
# 获取最近N天的统计
-
def self.recent_statistics(days = 7)
-
where(stats_date: (Date.current - days.days)..Date.current)
-
.order(stats_date: :desc)
-
end
-
-
private
-
-
def set_generated_at
-
self.generated_at ||= Time.current
-
self.generated_by ||= 'system_auto'
-
end
-
end
-
class DailyLeading < ApplicationRecord
-
# 关联
-
belongs_to :reading_schedule
-
belongs_to :leader, class_name: "User"
-
-
# 验证
-
validates :reading_suggestion, presence: true
-
validates :questions, presence: true
-
validates :reading_schedule_id, uniqueness: { message: "今日已有领读内容" }
-
-
# API序列化方法 - 标准化API响应格式
-
def as_json_for_api(options = {})
-
current_user = options[:current_user]
-
-
result = {
-
id: id,
-
reading_suggestion: reading_suggestion,
-
questions: questions,
-
summary: summary,
-
created_at: created_at,
-
updated_at: updated_at,
-
leader: leader.as_json_for_api
-
}
-
-
# 添加阅读计划信息
-
if options[:include_schedule] && reading_schedule
-
result[:reading_schedule] = {
-
id: reading_schedule.id,
-
day_number: reading_schedule.day_number,
-
date: reading_schedule.date,
-
reading_progress: reading_schedule.reading_progress
-
}
-
end
-
-
# 添加活动信息
-
if options[:include_event] && reading_schedule&.reading_event
-
result[:reading_event] = {
-
id: reading_schedule.reading_event.id,
-
title: reading_schedule.reading_event.title
-
}
-
end
-
-
# 添加当前用户的权限信息
-
if current_user
-
result[:interactions] = {
-
can_edit: can_edit?(current_user),
-
is_leader: leader_id == current_user.id
-
}
-
end
-
-
result
-
end
-
-
private
-
-
# 权限检查方法
-
def can_edit?(current_user)
-
return false unless current_user
-
return true if current_user.any_admin? # 管理员可以编辑任何领读内容
-
return true if leader_id == current_user.id # 领读人可以编辑自己的内容
-
false
-
end
-
end
-
1
class Enrollment < ApplicationRecord
-
# 关联
-
1
belongs_to :user
-
1
belongs_to :reading_event
-
1
has_many :check_ins, dependent: :destroy
-
-
# 验证
-
1
validates :user_id, uniqueness: { scope: :reading_event_id, message: "已经报名该活动" }
-
-
# 枚举
-
1
enum :payment_status, { unpaid: 0, paid: 1, refunded: 2 }
-
1
enum :role, { participant: 0, leader: 1 }
-
-
# 计算打卡完成率
-
1
def completion_rate
-
total_days = reading_event.reading_schedules.count
-
return 0 if total_days.zero?
-
-
completed_days = check_ins.where.not(status: :missed).count
-
(completed_days.to_f / total_days * 100).round(2)
-
end
-
-
# 计算应退押金
-
1
def refund_amount_calculated
-
reading_event.deposit * (completion_rate / 100.0)
-
end
-
-
# 权限检查方法
-
1
def is_current_leader?
-
return false unless reading_event&.in_progress?
-
reading_event.leader_id == user_id
-
end
-
-
1
def is_current_daily_leader?(schedule)
-
return false unless reading_event&.in_progress?
-
return false unless schedule&.reading_event_id == reading_event_id
-
-
# 检查是否是当天的领读人
-
schedule.daily_leader_id == user_id &&
-
schedule.date == Date.today
-
end
-
-
1
def is_current_participant?
-
reading_event&.in_progress? || reading_event&.enrolling?
-
end
-
-
# 活动结束时重置角色
-
1
def reset_roles_on_event_completion!
-
return unless reading_event&.completed?
-
-
update!(role: :participant) # 所有人都变回普通参与者
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: event_enrollments
-
#
-
# id :integer not null, primary key
-
# reading_event_id :integer not null
-
# user_id :integer not null
-
# enrollment_type :string default("participant"), not null
-
# status :string default("enrolled"), not null
-
# enrollment_date :datetime not null
-
# completion_rate :decimal(5, 2) default(0.0), not null
-
# check_ins_count :integer default(0), not null
-
# leader_days_count :integer default(0), not null
-
# flowers_received_count :integer default(0), not null
-
# fee_paid_amount :decimal(10, 2) default(0.0), not null
-
# fee_refund_amount :decimal(10, 2) default(0.0), not null
-
# refund_status :string default("pending"), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# idx_event_enrollments_enrollment_date (enrollment_date)
-
# idx_event_enrollments_enrollment_type (enrollment_type)
-
# idx_event_enrollments_status (status)
-
# index_event_enrollments_on_reading_event_id_and_user_id (reading_event_id, user_id) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (reading_event_id => reading_events.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
-
1
class EventEnrollment < ApplicationRecord
-
# 参与类型枚举
-
1
enum :enrollment_type, {
-
participant: 'participant', # 参与者
-
observer: 'observer' # 围观者
-
}, default: :participant
-
-
# 报名状态枚举
-
1
enum :status, {
-
enrolled: 'enrolled', # 已报名
-
completed: 'completed', # 已完成
-
cancelled: 'cancelled' # 已取消
-
}, default: :enrolled
-
-
# 退款状态枚举
-
1
enum :refund_status, {
-
pending: 'pending', # 待处理
-
refunded: 'refunded', # 已退款
-
forfeited: 'forfeited' # 没收
-
}, default: :pending
-
-
# 关联关系
-
1
belongs_to :reading_event
-
1
belongs_to :user
-
-
1
has_many :check_ins, dependent: :destroy
-
1
has_many :received_flowers, class_name: 'Flower', foreign_key: :recipient_id, dependent: :destroy
-
1
has_many :given_flowers, class_name: 'Flower', foreign_key: :giver_id, dependent: :destroy
-
1
has_many :daily_leading_assignments, class_name: 'ReadingSchedule', foreign_key: :daily_leader_id, dependent: :nullify
-
1
has_many :participation_certificates, dependent: :destroy
-
-
# 验证规则
-
1
validates :enrollment_date, presence: true
-
1
validates :completion_rate, numericality: {
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 100
-
}
-
1
validates :fee_paid_amount, numericality: {
-
greater_than_or_equal_to: 0
-
}
-
1
validates :fee_refund_amount, numericality: {
-
greater_than_or_equal_to: 0
-
}
-
1
validate :cannot_enroll_if_event_completed
-
1
validate :unique_enrollment_per_event
-
-
# 作用域
-
1
scope :participants, -> { where(enrollment_type: :participant) }
-
1
scope :observers, -> { where(enrollment_type: :observer) }
-
1
scope :enrolled, -> { where(status: :enrolled) }
-
1
scope :completed, -> { where(status: :completed) }
-
1
scope :cancelled, -> { where(status: :cancelled) }
-
1
scope :active, -> { where(status: [:enrolled, :completed]) }
-
1
scope :by_completion_rate, ->(direction = :desc) { order(completion_rate: direction) }
-
1
scope :by_flowers_count, ->(direction = :desc) { order(flowers_received_count: direction) }
-
-
# 类方法:计算报名统计
-
1
def self.calculate_enrollment_statistics
-
all_enrollments = includes(:user, :reading_event)
-
-
{
-
total_enrollments: all_enrollments.count,
-
active_enrollments: all_enrollments.where(status: 'enrolled').count,
-
completed_enrollments: all_enrollments.where(status: 'completed').count,
-
cancelled_enrollments: all_enrollments.where(status: 'cancelled').count,
-
participants_count: all_enrollments.where(enrollment_type: 'participant').count,
-
observers_count: all_enrollments.where(enrollment_type: 'observer').count,
-
total_fees_collected: all_enrollments.sum(:fee_paid_amount),
-
total_refunds_processed: all_enrollments.sum(:fee_refund_amount),
-
enrollment_trend: calculate_enrollment_trend(all_enrollments),
-
completion_trend: calculate_completion_trend(all_enrollments)
-
}
-
end
-
-
# 委托方法
-
1
delegate :title, :book_name, :activity_mode, :completion_standard, to: :reading_event, prefix: true
-
1
delegate :nickname, to: :user, prefix: true
-
-
# 状态方法(公开方法供其他模型调用)
-
1
def can_participate?
-
enrolled? && participant?
-
end
-
-
1
def can_check_in?
-
can_participate? && reading_event.in_progress?
-
end
-
-
1
def can_receive_flowers?
-
can_participate? && check_ins.any?
-
end
-
-
1
def can_give_flowers?
-
can_participate? && reading_event.in_progress?
-
end
-
-
1
def can_cancel?
-
enrolled? && !reading_event.in_progress?
-
end
-
-
1
def cancellation_error_message
-
return "报名已取消,无法再次取消" if cancelled?
-
return "活动已开始,无法取消报名" if reading_event.in_progress?
-
return "活动已完成,无法取消报名" if reading_event.completed?
-
"无法取消报名"
-
end
-
-
1
def is_completed?
-
completion_rate >= reading_event.completion_standard
-
end
-
-
# 统计方法
-
1
def update_completion_rate!
-
new_rate = calculate_completion_rate
-
update!(completion_rate: new_rate)
-
-
# 如果完成率达到标准,更新状态
-
if is_completed? && enrolled?
-
update!(status: :completed)
-
end
-
end
-
-
1
def calculate_completion_rate
-
case reading_event.activity_mode
-
when 'note_checkin'
-
calculate_note_checkin_completion
-
when 'free_discussion'
-
calculate_free_discussion_completion
-
when 'video_conference'
-
calculate_video_conference_completion
-
when 'offline_meeting'
-
calculate_offline_meeting_completion
-
else
-
0.0
-
end
-
end
-
-
# 费用相关方法
-
1
def calculate_refund_amount
-
return 0.0 if reading_event.fee_type != 'deposit'
-
-
DepositRefundCalculator.calculate_refund_amount(user, reading_event)
-
end
-
-
1
def process_refund!
-
return unless reading_event.fee_type == 'deposit'
-
return if refund_status != 'pending'
-
-
refund_amount = calculate_refund_amount
-
-
transaction do
-
update!(
-
fee_refund_amount: refund_amount,
-
refund_status: refund_amount > 0 ? 'refunded' : 'forfeited'
-
)
-
-
# 这里应该调用实际的退款服务
-
# RefundService.process(user, refund_amount) if refund_amount > 0
-
end
-
end
-
-
# 证书相关方法
-
1
def eligible_for_completion_certificate?
-
is_completed? && participation_certificates.where(certificate_type: 'completion').empty?
-
end
-
-
1
def eligible_for_flower_certificate?(rank = nil)
-
return false unless flowers_received_count > 0
-
-
if rank
-
# 检查是否在指定排名
-
top_rankings = reading_event.event_enrollments
-
.where('flowers_received_count > 0')
-
.order(flowers_received_count: :desc)
-
.limit(rank)
-
-
top_rankings.include?(self) &&
-
participation_certificates.where(certificate_type: "flower_top#{rank}").empty?
-
else
-
# 只要有小红花就可能有资格
-
flowers_received_count > 0
-
end
-
end
-
-
# 通知方法
-
1
def notify_enrollment_confirmation
-
# 发送报名确认通知
-
EnrollmentNotificationService.confirm_enrollment(self)
-
end
-
-
1
def notify_completion_achievement
-
return unless is_completed?
-
-
# 发送完成成就通知
-
EnrollmentNotificationService.notify_completion(self)
-
end
-
-
1
def notify_certificate_issued(certificate)
-
# 发送证书颁发通知
-
EnrollmentNotificationService.notify_certificate_issued(self, certificate)
-
end
-
-
# API响应格式化
-
1
def as_json_for_api(options = {})
-
base_data = {
-
id: id,
-
enrollment_type: enrollment_type,
-
status: status,
-
enrollment_date: enrollment_date,
-
completion_rate: completion_rate,
-
check_ins_count: check_ins_count,
-
leader_days_count: leader_days_count,
-
flowers_received_count: flowers_received_count,
-
fee_paid_amount: fee_paid_amount,
-
fee_refund_amount: fee_refund_amount,
-
refund_status: refund_status,
-
created_at: created_at,
-
updated_at: updated_at,
-
is_completed: is_completed?,
-
can_participate: can_participate?,
-
can_check_in: can_check_in?,
-
can_receive_flowers: can_receive_flowers?,
-
can_give_flowers: can_give_flowers?,
-
can_cancel: can_cancel?
-
}
-
-
# 可选包含关联数据
-
if options[:include_user]
-
base_data[:user] = user.as_json_for_api
-
end
-
-
if options[:include_reading_event]
-
base_data[:reading_event] = reading_event.as_json_for_api
-
end
-
-
if options[:include_check_ins]
-
base_data[:check_ins] = check_ins.includes(:user).map do |check_in|
-
check_in.as_json_for_api(include_user: false)
-
end
-
end
-
-
if options[:include_flowers]
-
base_data[:flowers] = received_flowers.includes(:giver).map do |flower|
-
{
-
id: flower.id,
-
amount: flower.amount,
-
flower_type: flower.flower_type,
-
comment: flower.comment,
-
giver: flower.giver.as_json_for_api,
-
created_at: flower.created_at
-
}
-
end
-
end
-
-
if options[:include_certificates]
-
base_data[:certificates] = participation_certificates.map do |cert|
-
{
-
id: cert.id,
-
certificate_type: cert.certificate_type,
-
certificate_number: cert.certificate_number,
-
issued_at: cert.issued_at,
-
is_public: cert.is_public,
-
certificate_url: cert.certificate_url
-
}
-
end
-
end
-
-
if options[:include_statistics]
-
base_data[:statistics] = {
-
completion_percentage: completion_rate,
-
attendance_rate: reading_event.reading_schedules.any? ? (check_ins_count.to_f / reading_event.reading_schedules.count * 100).round(2) : 0,
-
flower_ranking_in_event: calculate_flower_ranking_in_event
-
}
-
end
-
-
base_data
-
end
-
-
1
private
-
-
# 验证方法
-
1
def cannot_enroll_if_event_completed
-
if reading_event.completed? && enrolled?
-
errors.add(:base, "不能报名已完成的活动")
-
end
-
end
-
-
1
def unique_enrollment_per_event
-
return unless reading_event_id && user_id
-
-
existing = EventEnrollment.where(
-
reading_event_id: reading_event_id,
-
user_id: user_id
-
).where.not(id: id)
-
-
if existing.exists?
-
errors.add(:base, "已经报名过此活动")
-
end
-
end
-
-
# 完成率计算方法
-
1
def calculate_note_checkin_completion
-
schedules = reading_event.reading_schedules
-
total_days = calculate_total_reading_days(schedules, reading_event)
-
-
return 0.0 if total_days == 0
-
-
# 获取实际打卡次数
-
check_ins_count = check_ins
-
.where(schedule: schedules)
-
.where.not(status: 'supplement')
-
.count
-
-
# 获取担任领读天数
-
leader_days_count = daily_leading_assignments
-
.where(reading_schedule: schedules)
-
.count
-
-
# 计算完成率:(打卡次数 + 担任领读天数) / 总天数
-
completed_days = check_ins_count + leader_days_count
-
(completed_days.to_f / total_days * 100).round(2)
-
end
-
-
1
def calculate_free_discussion_completion
-
# 自由讨论模式:基于参与度计算
-
# 这里可以基于发帖、回复等互动数据计算
-
# 暂时使用打卡次数作为基础指标
-
schedules = reading_event.reading_schedules
-
total_days = calculate_total_reading_days(schedules, reading_event)
-
-
return 0.0 if total_days == 0
-
-
participation_count = check_ins.where(schedule: schedules).count
-
(participation_count.to_f / total_days * 100).round(2)
-
end
-
-
1
def calculate_video_conference_completion
-
# 视频会议模式:基于出席率计算
-
# 这里需要检查用户的会议出席记录
-
# 暂时返回基于日程的简单计算
-
schedules = reading_event.reading_schedules
-
total_sessions = schedules.count
-
-
return 0.0 if total_sessions == 0
-
-
# 假设用户参与了所有会议(实际应该检查出席记录)
-
attendance_count = total_sessions # 这里应该是实际的出席次数
-
(attendance_count.to_f / total_sessions * 100).round(2)
-
end
-
-
1
def calculate_offline_meeting_completion
-
# 线下交流模式:基于出席率计算
-
# 类似视频会议模式,但针对线下活动
-
schedules = reading_event.reading_schedules
-
total_meetings = schedules.count
-
-
return 0.0 if total_meetings == 0
-
-
# 假设用户参与了所有会议(实际应该检查出席记录)
-
attendance_count = total_meetings # 这里应该是实际的出席次数
-
(attendance_count.to_f / total_meetings * 100).round(2)
-
end
-
-
1
def calculate_total_reading_days(schedules, event)
-
if event.weekend_rest
-
# 排除周末
-
schedules.where.not(date: [Date::SATURDAY, Date::SUNDAY]).count
-
else
-
schedules.count
-
end
-
end
-
-
1
def calculate_flower_ranking_in_event
-
return nil unless flowers_received_count > 0
-
-
# 获取活动中所有有小红花的参与者,按数量排序
-
ranked_participants = reading_event.event_enrollments
-
.where('flowers_received_count > 0')
-
.order(flowers_received_count: :desc)
-
.pluck(:id)
-
-
ranked_participants.index(id) + 1
-
end
-
end
-
1
class Flower < ApplicationRecord
-
# 关联
-
1
belongs_to :check_in
-
1
belongs_to :giver, class_name: "User"
-
1
belongs_to :recipient, class_name: "User"
-
1
belongs_to :reading_schedule
-
1
has_many :comments, as: :commentable, dependent: :destroy
-
-
# 验证
-
1
validates :check_in_id, uniqueness: { message: "该打卡已获得小红花" }
-
1
validate :daily_flower_limit
-
1
validate :giver_is_daily_leader
-
-
# 获取赠送者显示名称
-
1
def giver_display_name
-
return '匿名用户' if is_anonymous?
-
giver&.nickname || '未知用户'
-
end
-
-
# 获取接收者显示名称
-
1
def recipient_display_name
-
recipient&.nickname || '未知用户'
-
end
-
-
# 评论相关方法
-
1
def add_comment(user, content)
-
comments.create!(
-
user: user,
-
content: content,
-
commentable: self
-
)
-
end
-
-
1
def can_receive_comment?(user)
-
# 小红花接收者可以查看和回复评论
-
return true if user.present?
-
# 这里可以添加更多权限逻辑
-
true
-
end
-
-
1
def recent_comments(limit = 5)
-
comments.includes(:user).order(created_at: :desc).limit(limit)
-
end
-
-
1
def comments_count
-
comments.count
-
end
-
-
# API响应格式
-
1
def as_json_for_api(options = {})
-
base_data = {
-
id: id,
-
giver: is_anonymous? ? { id: nil, nickname: '匿名用户' } : giver.as_json_for_api,
-
recipient: recipient.as_json_for_api,
-
check_in: {
-
id: check_in.id,
-
content: check_in.content.truncate(100),
-
user: check_in.user.as_json_for_api,
-
created_at: check_in.created_at
-
},
-
amount: amount,
-
flower_type: flower_type,
-
comment: comment,
-
is_anonymous: is_anonymous,
-
created_at: created_at,
-
giver_display_name: giver_display_name,
-
recipient_display_name: recipient_display_name,
-
comments_count: comments_count
-
}
-
-
# 可选包含评论数据
-
if options[:include_comments]
-
base_data[:comments] = recent_comments.map(&:as_json_for_api)
-
end
-
-
if options[:include_comment_stats]
-
base_data[:comment_stats] = {
-
total_count: comments_count,
-
recent_count: recent_comments.count,
-
latest_comment: recent_comments.first&.as_json_for_api
-
}
-
end
-
-
base_data
-
end
-
-
1
private
-
-
# 每日最多发放3朵小红花
-
1
def daily_flower_limit
-
daily_count = Flower.where(
-
giver: giver,
-
reading_schedule: reading_schedule
-
).count
-
-
if daily_count >= 3 && !persisted?
-
errors.add(:base, "每日最多发放3朵小红花")
-
end
-
end
-
-
# 只有领读人可以发放小红花(考虑3天权限窗口)
-
1
def giver_is_daily_leader
-
return if reading_schedule.blank? || giver.blank?
-
-
# 检查是否有权限发放小红花(当天和后一天权限)
-
event = reading_schedule.reading_event
-
unless event&.can_give_flowers?(giver, reading_schedule)
-
errors.add(:base, "只有领读人可以在当天或后一天发放小红花")
-
end
-
end
-
end
-
1
class FlowerCertificate < ApplicationRecord
-
# 关联
-
1
belongs_to :user
-
1
belongs_to :reading_event
-
-
# 验证
-
1
validates :rank, inclusion: { in: [1, 2, 3] }
-
1
validates :total_flowers, numericality: { greater_than: 0 }
-
1
validates :certificate_id, presence: true, uniqueness: true
-
-
# 作用域
-
1
scope :for_user, ->(user) { where(user: user) }
-
1
scope :for_event, ->(event) { where(reading_event: event) }
-
1
scope :ranked, -> { order(:rank) }
-
-
# 回调
-
1
before_validation :generate_certificate_id, on: :create
-
-
# 实例方法
-
-
# 获取排名显示
-
1
def rank_display
-
case rank
-
when 1 then '🥇 第一名'
-
when 2 then '🥈 第二名'
-
when 3 then '🥉 第三名'
-
else "第#{rank}名"
-
end
-
end
-
-
# 获取荣誉等级
-
1
def honor_level
-
case rank
-
when 1 then '优秀小红花达人'
-
when 2 then '小红花之星'
-
when 3 then '小红花爱好者'
-
else '小红花参与者'
-
end
-
end
-
-
# 检查是否是前三名
-
1
def is_top_three?
-
rank <= 3
-
end
-
-
# 生成证书图片路径
-
1
def certificate_image_path
-
"/certificates/flower_certificate_#{certificate_id}.png"
-
end
-
-
# 生成证书分享链接
-
1
def share_url
-
"#{Rails.application.config.base_url}/flower_certificates/#{certificate_id}"
-
end
-
-
# 类方法
-
-
# 为活动生成前三名证书
-
1
def self.generate_top_three_certificates(event)
-
# 计算活动中的小红花排行榜
-
flower_stats = Flower.joins(:recipient)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: event.id })
-
.group('recipients.id')
-
.sum(:amount)
-
-
# 排序并获取前三名
-
top_users = flower_stats.sort_by { |user_id, flowers| -flowers }
-
.first(3)
-
.map.with_index(1) { |(user_id, flowers), index| [user_id, flowers, index] }
-
-
certificates = []
-
-
top_users.each do |user_id, total_flowers, rank|
-
user = User.find(user_id)
-
certificate = create!(
-
user: user,
-
reading_event: event,
-
rank: rank,
-
total_flowers: total_flowers
-
)
-
certificates << certificate
-
end
-
-
certificates
-
end
-
-
# 获取用户的所有小红花证书
-
1
def self.for_user_all(user)
-
for_user(user).ranked
-
end
-
-
# 检查证书是否有效
-
1
def valid_certificate?
-
reading_event&.status == 'completed'
-
end
-
-
# API响应格式
-
1
def as_json_for_api
-
{
-
id: id,
-
certificate_id: certificate_id,
-
rank: rank,
-
rank_display: rank_display,
-
honor_level: honor_level,
-
total_flowers: total_flowers,
-
user: user.as_json_for_api,
-
reading_event: reading_event.as_json_for_api,
-
is_top_three: is_top_three?,
-
valid_certificate: valid_certificate?,
-
share_url: share_url,
-
certificate_image_url: certificate_image_path,
-
created_at: created_at
-
}
-
end
-
-
1
private
-
-
# 生成唯一的证书编号
-
1
def generate_certificate_id
-
return if certificate_id.present?
-
-
loop do
-
id = "FC#{Time.current.strftime('%Y%m%d')}#{SecureRandom.hex(4).upcase}"
-
break self.certificate_id = id unless FlowerCertificate.exists?(certificate_id: id)
-
end
-
end
-
end
-
1
class FlowerQuota < ApplicationRecord
-
1
self.table_name = 'flower_quotas'
-
-
# 关联
-
1
belongs_to :user
-
1
belongs_to :reading_event
-
-
# 验证
-
1
validates :max_flowers, numericality: { greater_than: 0 }
-
1
validates :used_flowers, numericality: { greater_than_or_equal_to: 0 }
-
1
validates :quota_date, presence: true
-
-
# 作用域
-
1
scope :for_user, ->(user) { where(user: user) }
-
1
scope :for_event, ->(event) { where(reading_event: event) }
-
1
scope :for_date, ->(date) { where(quota_date: date) }
-
1
scope :current, -> { where(quota_date: Date.current) }
-
1
scope :recent, -> { order(quota_date: :desc) }
-
-
# 实例方法
-
-
# 检查是否还有赠送额度
-
1
def can_give_flower?(amount = 1)
-
(used_flowers + amount) <= max_flowers
-
end
-
-
# 获取剩余可赠送数量
-
1
def remaining_flowers
-
max_flowers - used_flowers
-
end
-
-
# 使用小红花(每日配额版本)
-
1
def use_flowers!(amount = 1)
-
return false unless can_give_flower?(amount)
-
-
transaction do
-
increment!(:used_flowers, amount)
-
increment!(:give_count_today, amount)
-
touch(:last_given_at)
-
end
-
true
-
end
-
-
# 重置使用数量(每日重置)
-
1
def reset_daily_usage!
-
update!(used_flowers: 0, give_count_today: 0)
-
end
-
-
# 获取使用率
-
1
def usage_percentage
-
return 0 if max_flowers == 0
-
(used_flowers.to_f / max_flowers * 100).round(2)
-
end
-
-
# 检查是否为今日配额
-
1
def for_today?
-
quota_date == Date.current
-
end
-
-
# 检查是否为活动日
-
1
def activity_day?(event)
-
event.start_date <= quota_date && quota_date <= event.end_date && !event.weekend_rest?
-
end
-
-
# 类方法
-
-
# 获取或创建每日配额
-
1
def self.get_or_create_daily_quota(user, event, date = Date.current, max_flowers: 3)
-
find_or_create_by(user: user, reading_event: event, quota_date: date) do |quota|
-
quota.max_flowers = max_flowers
-
quota.used_flowers = 0
-
quota.give_count_today = 0
-
end
-
end
-
-
# 兼容性方法 - 为用户和活动创建或获取配额
-
1
def self.get_or_create_quota(user, event, max_flowers: 3)
-
get_or_create_daily_quota(user, event, Date.current, max_flowers)
-
end
-
-
# 检查用户每日配额
-
1
def self.check_daily_quota(user, event, date = Date.current, amount = 1)
-
quota = find_by(user: user, reading_event: event, quota_date: date)
-
return { can_give: false, remaining: 0, is_activity_day: false } unless quota
-
-
{
-
can_give: quota.can_give_flower?(amount),
-
remaining: quota.remaining_flowers,
-
used: quota.used_flowers,
-
max: quota.max_flowers,
-
is_activity_day: quota.activity_day?(event),
-
quota_date: quota.quota_date
-
}
-
end
-
-
# 检查用户在活动中的配额(兼容性方法)
-
1
def self.check_quota(user, event, amount = 1)
-
result = check_daily_quota(user, event, Date.current, amount)
-
{
-
can_give: result[:can_give],
-
remaining: result[:remaining],
-
used: result[:used],
-
max: result[:max]
-
}
-
end
-
-
# 获取用户在活动中的历史配额
-
1
def self.user_quota_history(user, event, limit: 30)
-
for_user(user).for_event(event).recent.limit(limit)
-
end
-
-
# 获取活动在某日的总配额统计
-
1
def self.daily_quota_stats(event, date = Date.current)
-
quotas = for_event(event).for_date(date)
-
{
-
date: date,
-
total_users: quotas.count,
-
total_flowers_available: quotas.sum(:max_flowers),
-
total_flowers_used: quotas.sum(:used_flowers),
-
usage_rate: quotas.count > 0 ? (quotas.sum(:used_flowers).to_f / quotas.sum(:max_flowers) * 100).round(2) : 0
-
}
-
end
-
end
-
1
class Like < ApplicationRecord
-
1
belongs_to :user, counter_cache: :likes_given_count
-
1
belongs_to :target, polymorphic: true
-
-
# 验证
-
1
validates :user_id, uniqueness: { scope: [:target_type, :target_id] }
-
-
# 回调:维护目标对象的counter_cache
-
1
after_create :increment_target_counter
-
1
after_destroy :decrement_target_counter
-
-
# 类方法:创建点赞
-
1
def self.like!(user, target)
-
25
return false unless user && target
-
-
23
like = find_or_initialize_by(
-
user: user,
-
target: target
-
)
-
-
23
if like.new_record?
-
22
like.save!
-
22
true
-
else
-
1
false # 已经点赞
-
end
-
end
-
-
# 类方法:取消点赞
-
1
def self.unlike!(user, target)
-
6
return false unless user && target
-
-
4
like = find_by(
-
user: user,
-
target: target
-
)
-
-
4
if like
-
3
like.destroy!
-
3
true
-
else
-
1
false # 未点赞
-
end
-
end
-
-
# 类方法:检查是否点赞
-
1
def self.liked?(user, target)
-
11
return false unless user && target
-
9
exists?(user: user, target: target)
-
end
-
-
# API序列化方法 - 标准化API响应格式
-
1
def as_json_for_api(options = {})
-
result = {
-
id: id,
-
user: user.as_json_for_api,
-
target_type: target_type,
-
target_id: target_id,
-
created_at: created_at
-
}
-
-
# 添加目标对象信息
-
if options[:include_target] && target
-
result[:target] = if target.respond_to?(:as_json_for_api)
-
target.as_json_for_api(options)
-
else
-
{
-
type: target_type,
-
id: target.id,
-
title: target_title
-
}
-
end
-
end
-
-
result
-
end
-
-
1
private
-
-
# 获取目标对象的标题
-
1
def target_title
-
return unless target
-
-
case target_type
-
when 'Post'
-
target.title
-
when 'Comment'
-
target.content.truncate(50)
-
when 'CheckIn'
-
"第#{target.day_number}天打卡"
-
when 'ReadingEvent'
-
target.title
-
else
-
target_type
-
end
-
end
-
-
# 增加目标对象的计数器
-
1
def increment_target_counter
-
35
case target_type
-
when 'Post'
-
35
target.increment_likes_count if target.respond_to?(:increment_likes_count)
-
end
-
end
-
-
# 减少目标对象的计数器
-
1
def decrement_target_counter
-
5
case target_type
-
when 'Post'
-
5
target.decrement_likes_count if target.respond_to?(:decrement_likes_count)
-
end
-
end
-
end
-
# 通知模型
-
# 用于管理系统中各种用户通知,包括小红花相关通知、评论通知、活动通知等
-
1
class Notification < ApplicationRecord
-
# 关联关系
-
1
belongs_to :recipient, class_name: 'User'
-
1
belongs_to :actor, class_name: 'User'
-
1
belongs_to :notifiable, polymorphic: true
-
-
# 验证规则
-
1
validates :recipient, presence: true
-
1
validates :actor, presence: true
-
1
validates :notification_type, presence: true, inclusion: { in: %w[flower_received flower_comment activity_update event_approved event_rejected] }
-
1
validates :title, presence: true, length: { maximum: 100 }
-
1
validates :content, presence: true, length: { maximum: 500 }
-
-
# 作用域
-
1
scope :unread, -> { where(read: false) }
-
1
scope :read, -> { where(read: true) }
-
1
scope :recent, -> { order(created_at: :desc) }
-
1
scope :by_type, ->(type) { where(notification_type: type) }
-
1
scope :for_recipient, ->(user) { where(recipient: user) }
-
-
# 通知类型常量
-
NOTIFICATION_TYPES = {
-
1
flower_received: 'flower_received', # 收到小红花
-
flower_comment: 'flower_comment', # 小红花被评论
-
activity_update: 'activity_update', # 活动更新
-
event_approved: 'event_approved', # 活动审批通过
-
event_rejected: 'event_rejected' # 活动审批拒绝
-
}.freeze
-
-
# 默认排序
-
3
default_scope -> { order(created_at: :desc) }
-
-
# 实例方法
-
-
# 标记为已读
-
1
def mark_as_read!
-
update!(read: true, read_at: Time.current) unless read?
-
end
-
-
# 标记为未读
-
1
def mark_as_unread!
-
update!(read: false, read_at: nil)
-
end
-
-
# 是否已读
-
1
def read?
-
read
-
end
-
-
# 是否未读
-
1
def unread?
-
!read
-
end
-
-
# 获取通知的URL链接
-
1
def action_url
-
case notification_type
-
when 'flower_received', 'flower_comment'
-
if notifiable_type == 'Flower'
-
"/flowers/#{notifiable_id}"
-
elsif notifiable_type == 'Comment'
-
comment = Comment.find_by(id: notifiable_id)
-
if comment&.commentable_type == 'Flower'
-
"/flowers/#{comment.commentable_id}#comment-#{comment.id}"
-
end
-
end
-
when 'activity_update', 'event_approved', 'event_rejected'
-
if notifiable_type == 'ReadingEvent'
-
"/events/#{notifiable_id}"
-
end
-
else
-
'#'
-
end
-
end
-
-
# 获取通知图标类型
-
1
def icon_type
-
case notification_type
-
when 'flower_received'
-
'flower'
-
when 'flower_comment'
-
'comment'
-
when 'activity_update'
-
'activity'
-
when 'event_approved'
-
'approved'
-
when 'event_rejected'
-
'rejected'
-
else
-
'notification'
-
end
-
end
-
-
# 格式化创建时间
-
1
def formatted_created_at
-
case Time.current - created_at
-
when 0..59.seconds
-
'刚刚'
-
when 1..59.minutes
-
"#{(Time.current - created_at).to_i / 60}分钟前"
-
when 1..23.hours
-
"#{(Time.current - created_at).to_i / 3600}小时前"
-
when 1..29.days
-
"#{(Time.current - created_at).to_i / 86400}天前"
-
else
-
created_at.strftime('%m-%d %H:%M')
-
end
-
end
-
-
# API响应格式
-
1
def as_json_for_api(options = {})
-
base_data = {
-
id: id,
-
notification_type: notification_type,
-
title: title,
-
content: content,
-
read: read,
-
read_at: read_at,
-
created_at: created_at,
-
formatted_created_at: formatted_created_at,
-
action_url: action_url,
-
icon_type: icon_type
-
}
-
-
# 包含关联数据
-
if options[:include_actor]
-
base_data[:actor] = actor.as_json_for_api
-
end
-
-
if options[:include_notifiable]
-
base_data[:notifiable] = if notifiable
-
{
-
type: notifiable_type,
-
id: notifiable_id,
-
data: notifiable.as_json_for_api
-
}
-
else
-
nil
-
end
-
end
-
-
base_data
-
end
-
-
# 类方法
-
-
# 创建小红花通知
-
1
def self.create_flower_notification(recipient, actor, flower)
-
create!(
-
recipient: recipient,
-
actor: actor,
-
notifiable: flower,
-
notification_type: NOTIFICATION_TYPES[:flower_received],
-
title: '收到小红花',
-
content: "#{actor.nickname} 给了你一朵小红花:#{flower.comment.presence || '很棒的表现!'}"
-
)
-
end
-
-
# 创建评论通知
-
1
def self.create_comment_notification(recipient, actor, comment)
-
create!(
-
recipient: recipient,
-
actor: actor,
-
notifiable: comment,
-
notification_type: NOTIFICATION_TYPES[:flower_comment],
-
title: '新的评论',
-
content: "#{actor.nickname} 评论了你的小红花:#{comment.content.truncate(50)}"
-
)
-
end
-
-
# 创建活动更新通知
-
1
def self.create_activity_notification(recipient, actor, event, update_type, message)
-
create!(
-
recipient: recipient,
-
actor: actor,
-
notifiable: event,
-
notification_type: NOTIFICATION_TYPES[:activity_update],
-
title: '活动更新',
-
content: message
-
)
-
end
-
-
# 创建活动审批通知
-
1
def self.create_event_approval_notification(recipient, actor, event, approved)
-
notification_type = approved ? NOTIFICATION_TYPES[:event_approved] : NOTIFICATION_TYPES[:event_rejected]
-
title = approved ? '活动审批通过' : '活动审批拒绝'
-
-
create!(
-
recipient: recipient,
-
actor: actor,
-
notifiable: event,
-
notification_type: notification_type,
-
title: title,
-
content: "#{actor.nickname} #{approved ? '通过了' : '拒绝了'}你的活动申请:#{event.title}"
-
)
-
end
-
-
# 批量标记为已读
-
1
def self.mark_all_as_read_for(recipient)
-
where(recipient: recipient, read: false).update_all(read: true, read_at: Time.current)
-
end
-
-
# 获取用户未读通知数量
-
1
def self.unread_count_for(recipient)
-
where(recipient: recipient, read: false).count
-
end
-
-
# 获取用户最近的通知
-
1
def self.recent_for(recipient, limit = 10)
-
where(recipient: recipient).limit(limit)
-
end
-
-
# 清理过期通知(保留30天)
-
1
def self.cleanup_old_notifications(days = 30)
-
where('created_at < ?', days.days.ago).delete_all
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: participation_certificates
-
#
-
# id :integer not null, primary key
-
# reading_event_id :integer not null
-
# user_id :integer not null
-
# certificate_type :string not null
-
# certificate_number: string not null
-
# issued_at :datetime not null
-
# achievement_data :text
-
# certificate_url :string
-
# is_public :boolean default(TRUE), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# idx_certificates_event_id (reading_event_id)
-
# idx_certificates_is_public (is_public)
-
# idx_certificates_issued_at (issued_at)
-
# idx_certificates_type (certificate_type)
-
# idx_certificates_user_id (user_id)
-
# index_participation_certificates_on_certificate_number (certificate_number) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (reading_event_id => reading_events.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
-
class ParticipationCertificate < ApplicationRecord
-
# 证书类型枚举
-
enum :certificate_type, {
-
completion: 'completion', # 完成证书
-
flower_top1: 'flower_top1', # 小红花第一名证书
-
flower_top2: 'flower_top2', # 小红花第二名证书
-
flower_top3: 'flower_top3', # 小红花第三名证书
-
custom: 'custom' # 自定义证书
-
}, default: :completion
-
-
# 关联关系
-
belongs_to :reading_event
-
belongs_to :user
-
-
# 验证规则
-
validates :certificate_number, presence: true, uniqueness: true
-
validates :issued_at, presence: true
-
validates :achievement_data, presence: true
-
validate :certificate_number_format
-
validate :user_must_be_event_participant
-
validate :certificate_requirements_met
-
-
# 作用域
-
scope :is_public, -> { where(is_public: true) }
-
scope :is_private, -> { where(is_public: false) }
-
scope :completion, -> { where(certificate_type: :completion) }
-
scope :flower_top, -> { where(certificate_type: [:flower_top1, :flower_top2, :flower_top3]) }
-
scope :custom, -> { where(certificate_type: :custom) }
-
scope :recent, -> { order(issued_at: :desc) }
-
-
# 委托方法
-
delegate :title, :book_name, to: :reading_event, prefix: true
-
delegate :nickname, to: :user, prefix: true
-
-
# 状态方法
-
def completion_certificate?
-
certificate_type == 'completion'
-
end
-
-
def flower_certificate?
-
['flower_top1', 'flower_top2', 'flower_top3'].include?(certificate_type)
-
end
-
-
def custom_certificate?
-
certificate_type == 'custom'
-
end
-
-
def flower_rank
-
return nil unless flower_certificate?
-
-
case certificate_type
-
when 'flower_top1' then 1
-
when 'flower_top2' then 2
-
when 'flower_top3' then 3
-
end
-
end
-
-
# 证书生成方法
-
def self.generate_completion_certificate(enrollment)
-
return nil unless enrollment.is_completed?
-
return nil if exists?(user: enrollment.user, reading_event: enrollment.reading_event, certificate_type: :completion)
-
-
certificate_number = generate_certificate_number('COMP')
-
achievement_data = build_completion_achievement_data(enrollment)
-
-
create!(
-
user: enrollment.user,
-
reading_event: enrollment.reading_event,
-
certificate_type: :completion,
-
certificate_number: certificate_number,
-
issued_at: Time.current,
-
achievement_data: achievement_data.to_json,
-
is_public: true
-
)
-
end
-
-
def self.generate_flower_certificate(enrollment, rank)
-
return nil unless enrollment.flowers_received_count > 0
-
return nil if rank < 1 || rank > 3
-
-
certificate_type = "flower_top#{rank}".to_sym
-
return nil if exists?(user: enrollment.user, reading_event: enrollment.reading_event, certificate_type: certificate_type)
-
-
certificate_number = generate_certificate_number('FLOWER')
-
achievement_data = build_flower_achievement_data(enrollment, rank)
-
-
create!(
-
user: enrollment.user,
-
reading_event: enrollment.reading_event,
-
certificate_type: certificate_type,
-
certificate_number: certificate_number,
-
issued_at: Time.current,
-
achievement_data: achievement_data.to_json,
-
is_public: true
-
)
-
end
-
-
def self.generate_custom_certificate(enrollment, custom_data = {})
-
certificate_number = generate_certificate_number('CUSTOM')
-
achievement_data = build_custom_achievement_data(enrollment, custom_data)
-
-
create!(
-
user: enrollment.user,
-
reading_event: enrollment.reading_event,
-
certificate_type: :custom,
-
certificate_number: certificate_number,
-
issued_at: Time.current,
-
achievement_data: achievement_data.to_json,
-
is_public: custom_data[:is_public] != false
-
)
-
end
-
-
# 证书内容方法
-
def certificate_title
-
case certificate_type
-
when 'completion'
-
"#{reading_event.title} 完成证书"
-
when 'flower_top1'
-
"#{reading_event.title} 小红花冠军证书"
-
when 'flower_top2'
-
"#{reading_event.title} 小红花亚军证书"
-
when 'flower_top3'
-
"#{reading_event.title} 小红花季军证书"
-
when 'custom'
-
"#{reading_event.title} 荣誉证书"
-
end
-
end
-
-
def certificate_description
-
case certificate_type
-
when 'completion'
-
"完成#{reading_event.days_count}天共读活动,完成率达到#{enrollment.completion_rate}%"
-
when 'flower_top1'
-
"在#{reading_event.title}活动中获得小红花数量第一名(#{enrollment.flowers_received_count}朵)"
-
when 'flower_top2'
-
"在#{reading_event.title}活动中获得小红花数量第二名(#{enrollment.flowers_received_count}朵)"
-
when 'flower_top3'
-
"在#{reading_event.title}活动中获得小红花数量第三名(#{enrollment.flowers_received_count}朵)"
-
when 'custom'
-
achievement_data['description'] || "在#{reading_event.title}活动中表现优异"
-
end
-
end
-
-
def achievement_info
-
return {} unless achievement_data.is_a?(String) || achievement_data.is_a?(Hash)
-
-
data = achievement_data.is_a?(String) ? JSON.parse(achievement_data) : achievement_data
-
data.with_indifferent_access
-
end
-
-
def enrollment
-
@enrollment ||= reading_event.event_enrollments.find_by(user: user)
-
end
-
-
# 分享方法
-
def shareable_url
-
# 生成证书分享链接
-
"/certificates/#{certificate_number}"
-
end
-
-
def shareable_image_url
-
# 生成证书图片URL
-
certificate_url || "/certificates/#{certificate_number}/image"
-
end
-
-
def can_share?
-
is_public? && certificate_url.present?
-
end
-
-
# 验证方法
-
def verify_certificate
-
{
-
valid: true,
-
certificate_number: certificate_number,
-
holder_name: user.nickname,
-
event_name: reading_event.title,
-
issue_date: issued_at.strftime('%Y年%m月%d日'),
-
certificate_type: certificate_type,
-
verification_code: generate_verification_code
-
}
-
end
-
-
private
-
-
# 证书编号生成
-
def self.generate_certificate_number(prefix)
-
timestamp = Time.current.strftime('%Y%m%d')
-
random = SecureRandom.hex(4).upcase
-
"#{prefix}-#{timestamp}-#{random}"
-
end
-
-
# 成就数据构建
-
def self.build_completion_achievement_data(enrollment)
-
{
-
completion_rate: enrollment.completion_rate,
-
check_ins_count: enrollment.check_ins_count,
-
leader_days_count: enrollment.leader_days_count,
-
flowers_received_count: enrollment.flowers_received_count,
-
event_duration: enrollment.reading_event.days_count,
-
book_name: enrollment.reading_event.book_name,
-
activity_mode: enrollment.reading_event.activity_mode,
-
issue_date: Time.current.iso8601
-
}
-
end
-
-
def self.build_flower_achievement_data(enrollment, rank)
-
{
-
rank: rank,
-
flowers_count: enrollment.flowers_received_count,
-
completion_rate: enrollment.completion_rate,
-
check_ins_count: enrollment.check_ins_count,
-
book_name: enrollment.reading_event.book_name,
-
total_participants: enrollment.reading_event.participants_count,
-
issue_date: Time.current.iso8601
-
}
-
end
-
-
def self.build_custom_achievement_data(enrollment, custom_data)
-
{
-
description: custom_data[:description],
-
custom_fields: custom_data[:custom_fields] || {},
-
completion_rate: enrollment.completion_rate,
-
check_ins_count: enrollment.check_ins_count,
-
flowers_received_count: enrollment.flowers_received_count,
-
book_name: enrollment.reading_event.book_name,
-
issue_date: Time.current.iso8601
-
}
-
end
-
-
# 验证方法
-
def certificate_number_format
-
return unless certificate_number
-
-
unless certificate_number.match?(/\A[A-Z]+-\d{8}-[A-F0-9]{8}\z/)
-
errors.add(:certificate_number, "格式不正确")
-
end
-
end
-
-
def user_must_be_event_participant
-
return unless user && reading_event
-
-
unless reading_event.participants.include?(user)
-
errors.add(:user, "不是该活动的参与者")
-
end
-
end
-
-
def certificate_requirements_met
-
return unless user && reading_event && certificate_type
-
-
case certificate_type
-
when 'completion'
-
unless enrollment&.is_completed?
-
errors.add(:base, "用户未达到完成证书的颁发条件")
-
end
-
when 'flower_top1', 'flower_top2', 'flower_top3'
-
unless enrollment&.flowers_received_count&.positive?
-
errors.add(:base, "用户未达到小红花证书的颁发条件")
-
end
-
end
-
end
-
-
def generate_verification_code
-
# 生成验证码用于证书验证
-
Digest::MD5.hexdigest("#{certificate_number}-#{user_id}-#{reading_event_id}")[0, 8].upcase
-
end
-
end
-
1
class Post < ApplicationRecord
-
1
belongs_to :user, counter_cache: :posts_count
-
1
has_many :comments, dependent: :destroy, counter_cache: true
-
1
has_many :likes, as: :target, dependent: :destroy
-
-
# 验证
-
1
validates :title, presence: true, length: { maximum: 100 }
-
1
validates :content, presence: true, length: { minimum: 10, maximum: 5000 }
-
1
validates :category, inclusion: { in: %w[reading activity chat help], allow_blank: true }
-
-
# 回调:手动维护多态关联的counter_cache
-
1
after_create :initialize_counters
-
-
# 作用域
-
2
scope :visible, -> { where(hidden: false) }
-
2
scope :pinned_first, -> { order(pinned: :desc, created_at: :desc) }
-
1
scope :by_category, ->(category) { where(category: category) if category.present? }
-
-
# 权限检查方法
-
1
def can_edit?(current_user)
-
7
return false unless current_user
-
6
return true if current_user.any_admin? # 管理员可以编辑任何帖子
-
3
return true if user_id == current_user.id # 作者可以编辑自己的帖子
-
2
false
-
end
-
-
1
def can_hide?(current_user)
-
4
current_user&.any_admin?
-
end
-
-
1
def can_pin?(current_user)
-
4
current_user&.any_admin?
-
end
-
-
# 管理员操作方法
-
1
def hide!
-
1
update!(hidden: true)
-
end
-
-
1
def unhide!
-
1
update!(hidden: false)
-
end
-
-
1
def pin!
-
1
update!(pinned: true)
-
end
-
-
1
def unpin!
-
1
update!(pinned: false)
-
end
-
-
# 公共辅助方法
-
1
def can_edit_current_user
-
# 这个方法会在控制器中设置
-
4
@can_edit_current_user || false
-
end
-
-
1
def time_ago
-
8
time_ago_in_words(created_at)
-
end
-
-
1
def time_ago_in_words(time)
-
8
seconds = Time.current - time
-
8
minutes = seconds / 60
-
8
hours = minutes / 60
-
8
days = hours / 24
-
-
8
if days >= 1
-
1
"#{days.to_i}天前"
-
7
elsif hours >= 1
-
1
"#{hours.to_i}小时前"
-
6
elsif minutes >= 1
-
1
"#{minutes.to_i}分钟前"
-
else
-
5
"刚刚"
-
end
-
end
-
-
# 获取分类名称
-
1
def category_name
-
category_map = {
-
4
'reading' => '读书心得',
-
'activity' => '活动讨论',
-
'chat' => '闲聊区',
-
'help' => '求助问答'
-
}
-
4
category_map[category] || '全部'
-
end
-
-
# 统计点赞数 - 使用counter_cache
-
# 注意:需要手动维护多态关联的counter_cache
-
1
def likes_count
-
110
self[:likes_count] || likes.count
-
end
-
-
# 统计评论数 - 使用counter_cache
-
1
def comments_count
-
110
self[:comments_count] || comments.count
-
end
-
-
# 检查当前用户是否点赞
-
1
def liked_by?(current_user)
-
return false unless current_user
-
likes.exists?(user_id: current_user.id)
-
end
-
-
# 检查当前用户是否点赞(用于JSON序列化)
-
1
def liked_by_current_user
-
4
current_user = @current_user
-
4
return false unless current_user
-
liked_by?(current_user)
-
end
-
-
# API序列化方法 - 标准化API响应格式
-
1
def as_json_for_api(options = {})
-
current_user = options[:current_user]
-
-
result = {
-
id: id,
-
title: title,
-
content: content,
-
category: category,
-
category_name: category_name,
-
pinned: pinned,
-
hidden: hidden,
-
created_at: created_at,
-
updated_at: updated_at,
-
time_ago: time_ago_in_words(created_at),
-
stats: {
-
likes_count: likes_count,
-
comments_count: comments_count
-
},
-
author: user.as_json_for_api
-
}
-
-
# 添加标签信息
-
if options[:include_tags] && respond_to?(:tags)
-
result[:tags] = tags
-
end
-
-
# 添加当前用户的交互状态
-
if current_user
-
result[:interactions] = {
-
liked: liked_by?(current_user),
-
can_edit: can_edit?(current_user),
-
can_hide: can_hide?(current_user),
-
can_pin: can_pin?(current_user)
-
}
-
end
-
-
# 包含关联数据
-
if options[:include_comments]
-
result[:recent_comments] = comments.limit(5).map(&:as_json_for_api)
-
end
-
-
if options[:include_likes]
-
result[:recent_likes] = likes.limit(10).includes(:user).map do |like|
-
{
-
id: like.id,
-
user: like.user.as_json_for_api,
-
created_at: like.created_at
-
}
-
end
-
end
-
-
result
-
end
-
-
# JSON 序列化方法 - 保持向后兼容
-
1
def as_json(options = {})
-
4
super({
-
methods: [:author_info, :can_edit_current_user, :time_ago, :category_name, :likes_count, :comments_count, :tags, :liked_by_current_user],
-
include: {
-
user: {
-
only: [:id, :nickname, :avatar_url]
-
}
-
}
-
}.merge(options))
-
end
-
-
1
private
-
-
1
def author_info
-
{
-
4
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url,
-
role: user.role_display_name
-
}
-
end
-
-
# 初始化计数器
-
1
def initialize_counters
-
# 新帖子初始化为0
-
102
update_column(:likes_count, 0) if likes_count.nil?
-
102
update_column(:comments_count, 0) if comments_count.nil?
-
end
-
-
# 手动更新点赞计数器
-
1
def increment_likes_count
-
increment!(:likes_count)
-
end
-
-
1
def decrement_likes_count
-
decrement!(:likes_count)
-
end
-
end
-
1
class ReadingEvent < ApplicationRecord
-
# 活动状态枚举
-
1
enum :status, {
-
draft: 0, # 草稿
-
enrolling: 1, # 报名中
-
in_progress: 2, # 进行中
-
completed: 3 # 已完成
-
}, default: :draft
-
-
# 审批状态枚举
-
1
enum :approval_status, {
-
pending: 0, # 待审批
-
approved: 1, # 已批准
-
rejected: 2 # 已拒绝
-
}, default: :pending
-
-
# 活动模式枚举
-
1
enum :activity_mode, {
-
note_checkin: 'note_checkin', # 笔记打卡
-
free_discussion: 'free_discussion', # 自由讨论
-
video_conference: 'video_conference', # 视频会议
-
offline_meeting: 'offline_meeting' # 线下交流
-
}, default: :note_checkin
-
-
# 领读方式枚举
-
1
enum :leader_assignment_type, {
-
voluntary: 'voluntary', # 自由领读
-
random: 'random', # 随机领读
-
disabled: 'disabled' # 无领读
-
}, default: :voluntary
-
-
# 费用类型枚举
-
1
enum :fee_type, {
-
free: 'free', # 免费
-
deposit: 'deposit', # 押金制
-
paid: 'paid' # 收费制
-
}, default: :free
-
-
# 关联关系
-
1
belongs_to :leader, class_name: 'User', foreign_key: :leader_id
-
1
belongs_to :approver, class_name: 'User', foreign_key: :approved_by_id, optional: true
-
1
belongs_to :escalated_by, class_name: 'User', foreign_key: :escalated_by_user_id, optional: true
-
-
1
has_many :event_enrollments, dependent: :destroy, class_name: 'EventEnrollment'
-
1
has_many :participants, through: :event_enrollments, source: :user
-
1
has_many :reading_schedules, dependent: :destroy
-
1
has_many :participation_certificates, dependent: :destroy
-
-
# 验证规则
-
1
validates :title, presence: true, length: { minimum: 5, maximum: 100 }
-
1
validates :book_name, presence: true, length: { minimum: 2, maximum: 100 }
-
1
validates :start_date, :end_date, presence: true
-
1
validates :max_participants, numericality: {
-
greater_than: 0,
-
less_than_or_equal_to: 50
-
}
-
1
validates :min_participants, numericality: {
-
greater_than: 0,
-
63
less_than_or_equal_to: ->(event) { event.max_participants || 50 }
-
}
-
1
validates :fee_amount, numericality: {
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 500
-
}
-
1
validates :leader_reward_percentage, numericality: {
-
greater_than_or_equal_to: 0,
-
less_than_or_equal_to: 100
-
}
-
1
validates :completion_standard, numericality: {
-
greater_than_or_equal_to: 60,
-
less_than_or_equal_to: 100
-
}
-
1
validate :end_date_after_start_date
-
1
validate :enrollment_deadline_before_start_date, if: :enrollment_deadline?
-
1
validate :min_participants_not_greater_than_max
-
-
# 作用域
-
1
scope :with_details, -> { includes(:leader, :reading_schedules, :event_enrollments => :user) }
-
1
scope :filter_by_status, ->(status) { where(status: status) if status.present? }
-
1
scope :filter_by_mode, ->(mode) { where(activity_mode: mode) if mode.present? }
-
1
scope :filter_by_fee_type, ->(fee_type) { where(fee_type: fee_type) if fee_type.present? }
-
1
scope :upcoming, -> { where('start_date > ?', Date.current) }
-
1
scope :active, -> { where(status: [:enrolling, :in_progress]) }
-
1
scope :enrolling, -> { where(status: :enrolling) }
-
1
scope :in_progress, -> { where(status: :in_progress) }
-
1
scope :completed, -> { where(status: :completed) }
-
-
# 委托方法
-
1
delegate :nickname, to: :leader, prefix: true
-
-
# 计算方法
-
1
def service_fee
-
1
fee_amount * 0.2
-
end
-
-
1
def deposit
-
1
fee_amount * 0.8
-
end
-
-
1
def days_count
-
3
return 0 unless start_date && end_date
-
1
(end_date - start_date).to_i + 1
-
end
-
-
# 审批相关方法
-
1
def approve!(admin_user)
-
1
update!(
-
approval_status: :approved,
-
approved_by_id: admin_user.id,
-
approved_at: Time.current
-
)
-
end
-
-
1
def reject!(admin_user, reason = nil)
-
1
update!(
-
approval_status: :rejected,
-
approved_by_id: admin_user.id,
-
approved_at: Time.current,
-
rejection_reason: reason
-
)
-
end
-
-
1
def approved?
-
7
approval_status == 'approved'
-
end
-
-
1
def pending_approval?
-
1
approval_status == 'pending'
-
end
-
-
1
def rejected?
-
1
approval_status == 'rejected'
-
end
-
-
# 审批工作流相关方法
-
1
def can_submit_for_approval?
-
draft? && !submitted_for_approval_at.present?
-
end
-
-
1
def can_resubmit_for_approval?
-
rejected? && rejection_reason.present?
-
end
-
-
1
def can_be_approved_by?(admin_user)
-
pending_approval? && admin_user.can_approve_events?
-
end
-
-
1
def can_be_rejected_by?(admin_user)
-
pending_approval? && admin_user.can_approve_events?
-
end
-
-
1
def submit_for_approval!(workflow_type = :standard)
-
return false unless can_submit_for_approval?
-
-
service = ActivityApprovalWorkflowService.submit_for_approval!(self, workflow_type: workflow_type)
-
service.success?
-
end
-
-
1
def process_approval!(admin_user, reason: nil, notes: nil)
-
return false unless can_be_approved_by?(admin_user)
-
-
service = ActivityApprovalWorkflowService.approve!(self, admin_user, reason: reason, notes: notes)
-
service.success?
-
end
-
-
1
def process_rejection!(admin_user, reason, notes: nil)
-
return false unless can_be_rejected_by?(admin_user)
-
-
service = ActivityApprovalWorkflowService.reject!(self, admin_user, reason, notes: notes)
-
service.success?
-
end
-
-
1
def escalate_approval!(admin_user, escalation_reason)
-
service = ActivityApprovalWorkflowService.escalate!(self, admin_user, escalation_reason)
-
service.success?
-
end
-
-
# 领读人分配方法
-
1
def assign_daily_leaders!(assignment_type = nil, options = {})
-
3
return unless approved? && reading_schedules.any?
-
-
# 使用增强的LeaderAssignmentService
-
3
service = LeaderAssignmentService.auto_assign_leaders!(self, assignment_type: assignment_type, options: options)
-
3
service.success?
-
end
-
-
1
def assign_random_leaders!
-
1
assign_daily_leaders!(:random)
-
end
-
-
# 活动完成时重置所有角色
-
1
def complete_event!
-
1
transaction do
-
1
update!(status: :completed)
-
-
# 重置所有参与者的角色
-
1
enrollments.each do |enrollment|
-
enrollment.reset_roles_on_event_completion!
-
end
-
-
# 生成活动总结(可选)
-
generate_event_summary
-
end
-
end
-
-
# 检查当前用户是否是有效的小组长
-
1
def current_leader?(user)
-
9
return false unless in_progress?
-
5
leader_id == user.id
-
end
-
-
# 检查当前用户是否是有效的领读人(3天权限窗口)
-
1
def current_daily_leader?(user, schedule = nil)
-
1
return false unless in_progress?
-
-
1
if schedule.present?
-
return false unless schedule.reading_event_id == id
-
return false unless schedule.daily_leader_id == user.id
-
-
# 3天权限窗口:前一天、当天、后一天
-
leader_date = schedule.date
-
today = Date.today
-
-
# 检查今天是否在领读人的权限窗口内
-
(leader_date - 1.day) <= today && today <= (leader_date + 1.day)
-
else
-
# 查找用户作为领读人的所有schedule,检查是否在权限窗口内
-
1
user_schedules = reading_schedules.where(daily_leader: user)
-
1
return false if user_schedules.empty?
-
-
user_schedules.any? do |schedule|
-
leader_date = schedule.date
-
today = Date.today
-
(leader_date - 1.day) <= today && today <= (leader_date + 1.day)
-
end
-
end
-
end
-
-
# 检查用户是否有权限发布领读内容(前一天权限 + 小组长补位)
-
1
def can_publish_leading_content?(user, schedule)
-
1
return false unless in_progress?
-
1
return false unless schedule.reading_event_id == id
-
-
# 小组长全程具备发布权限(补位机制)
-
1
return true if current_leader?(user)
-
-
# 领读人权限检查
-
return false unless schedule.daily_leader_id == user.id
-
-
# 允许前一天发布领读内容
-
schedule.date >= Date.today
-
end
-
-
# 检查用户是否有权限发放小红花(当天和后一天权限 + 小组长补位)
-
1
def can_give_flowers?(user, schedule)
-
1
return false unless in_progress?
-
-
# 小组长全程具备发小红花权限(补位机制)
-
1
return true if current_leader?(user)
-
-
# 领读人权限检查
-
user_leading_schedules = reading_schedules.where(daily_leader: user)
-
return false if user_leading_schedules.empty?
-
-
# 检查是否有schedule在小红花发放权限窗口内
-
leader_dates = user_leading_schedules.pluck(:date)
-
today = Date.today
-
-
leader_dates.any? do |leader_date|
-
# 当天和后一天可以发小红花
-
leader_date <= today && today <= (leader_date + leader_flower_grace_period.days)
-
end
-
end
-
-
# 检查领读人是否缺失工作
-
1
def missing_leader_work?(date = Date.today)
-
return false unless in_progress?
-
-
schedule = reading_schedules.find_by(date: date)
-
return false unless schedule&.daily_leader.present?
-
-
# 检查是否缺失领读内容
-
missing_content = !schedule.daily_leading.present?
-
-
# 检查是否缺失小红花(如果是前一天或前两天的领读)
-
missing_flowers = false
-
if date <= Date.today && date >= Date.today - 2.days
-
schedule_date = date
-
flower_window_end = schedule_date + leader_flower_grace_period.days
-
-
if Date.today <= flower_window_end
-
# 还在小红花发放窗口内,检查是否已发放
-
check_ins_count = schedule.check_ins.count
-
flowers_count = schedule.flowers.count
-
-
# 有打卡但没有足够的小红花(建议至少1朵)
-
missing_flowers = check_ins_count > 0 && flowers_count == 0
-
end
-
end
-
-
{
-
schedule: schedule,
-
missing_content: missing_content,
-
missing_flowers: missing_flowers,
-
leader: schedule.daily_leader,
-
needs_backup: missing_content || missing_flowers
-
}
-
end
-
-
# 获取需要补位的日程列表
-
1
def schedules_need_backup
-
1
return [] unless in_progress?
-
-
# 检查最近3天的日程
-
1
date_range = (Date.today - 1.day)..(Date.today + 1.day)
-
1
schedules = reading_schedules.where(date: date_range).includes(:daily_leader, :daily_leading, :flowers, :check_ins)
-
-
1
backup_needed = []
-
-
1
schedules.each do |schedule|
-
# 检查领读内容是否缺失
-
content_missing = schedule.daily_leader.present? && !schedule.daily_leading.present?
-
-
# 检查小红花是否缺失
-
flowers_missing = false
-
if schedule.date <= Date.today && schedule.check_ins.any?
-
# 已经有打卡但没有小红花
-
flowers_missing = schedule.flowers.empty?
-
end
-
-
if content_missing || flowers_missing
-
backup_needed << {
-
schedule: schedule,
-
date: schedule.date,
-
day_number: schedule.day_number,
-
leader: schedule.daily_leader,
-
missing_content: content_missing,
-
missing_flowers: flowers_missing,
-
content_deadline: schedule.date,
-
flowers_deadline: schedule.date + leader_flower_grace_period.days
-
}
-
end
-
end
-
-
1
backup_needed
-
end
-
-
# 获取领读人权限窗口配置(可配置化)
-
1
def leader_permission_window
-
1
{
-
content_publish_days_before: 1, # 提前1天可以发布内容
-
content_publish_days_after: 0, # 当天后不能发布内容
-
flower_give_days_before: 0, # 当天前不能发小红花
-
flower_give_days_after: 1 # 当天后1天可以发小红花
-
}
-
end
-
-
# 统一的枚举访问方法 - 公有方法供Service使用
-
1
def status_symbol
-
status.to_sym
-
end
-
-
1
def approval_status_symbol
-
approval_status.to_sym
-
end
-
-
1
def leader_assignment_type_symbol
-
leader_assignment_type.to_sym
-
end
-
-
# 设置时也接受符号(可选,用于一致性)
-
1
def status_symbol=(value)
-
self.status = value.to_s
-
end
-
-
1
def approval_status_symbol=(value)
-
self.approval_status = value.to_s
-
end
-
-
1
def leader_assignment_type_symbol=(value)
-
self.leader_assignment_type = value.to_s
-
end
-
-
# 状态方法
-
1
def can_start?
-
enrolling? && start_date <= Date.current && enough_participants?
-
end
-
-
1
def can_enroll?
-
enrolling? && (enrollment_deadline.blank? || enrollment_deadline > Time.current) && !max_participants_reached?
-
end
-
-
# 报名相关辅助方法
-
1
def enrollment_error_message
-
return "活动不在报名状态" unless enrolling?
-
return "报名已截止" if enrollment_deadline.present? && enrollment_deadline <= Time.current
-
return "活动人数已满" if max_participants_reached?
-
return "活动尚未批准" unless approved?
-
"无法报名"
-
end
-
-
1
def user_enrolled?(user)
-
return false unless user
-
event_enrollments.where(user: user, status: 'enrolled').exists?
-
end
-
-
1
def user_enrollment(user)
-
return nil unless user
-
event_enrollments.find_by(user: user)
-
end
-
-
1
def enrollment_statistics
-
enrollments = event_enrollments.includes(:user)
-
-
{
-
total_enrollments: enrollments.count,
-
active_enrollments: enrollments.where(status: 'enrolled').count,
-
completed_enrollments: enrollments.where(status: 'completed').count,
-
cancelled_enrollments: enrollments.where(status: 'cancelled').count,
-
participants_count: enrollments.where(enrollment_type: 'participant').count,
-
observers_count: enrollments.where(enrollment_type: 'observer').count,
-
total_fees_collected: enrollments.sum(:fee_paid_amount),
-
total_refunds_processed: enrollments.sum(:fee_refund_amount),
-
enrollment_rate: calculate_enrollment_rate,
-
completion_rate: calculate_overall_completion_rate(enrollments)
-
}
-
end
-
-
1
def start!
-
return false unless can_start?
-
-
ActiveRecord::Base.transaction do
-
update!(status: :in_progress)
-
-
# 生成阅读计划(如果还没有)
-
generate_reading_schedules if reading_schedules.empty?
-
-
# 分配领读人
-
assign_daily_leaders! if leader_assignment_type != 'disabled'
-
-
true
-
end
-
end
-
-
1
def complete!
-
return false unless can_complete?
-
-
ActiveRecord::Base.transaction do
-
update!(status: :completed)
-
-
# 处理所有未完成的报名
-
event_enrollments.where(status: 'enrolled').each do |enrollment|
-
enrollment.update_completion_rate!
-
end
-
-
# 生成完成证书
-
generate_completion_certificates
-
-
true
-
end
-
end
-
-
1
def can_complete?
-
in_progress? && end_date < Date.current
-
end
-
-
1
def max_participants_reached?
-
event_enrollments.enrolled.count >= max_participants
-
end
-
-
1
def enough_participants?
-
event_enrollments.enrolled.count >= min_participants
-
end
-
-
1
def participants_count
-
event_enrollments.enrolled.count
-
end
-
-
1
def available_spots
-
max_participants - participants_count
-
end
-
-
# 统计方法
-
1
def completion_statistics
-
enrollments = event_enrollments.includes(:user)
-
-
{
-
total_participants: enrollments.count,
-
completed_participants: enrollments.where('completion_rate >= ?', completion_standard).count,
-
average_completion_rate: enrollments.average(:completion_rate)&.round(2) || 0,
-
total_check_ins: enrollments.sum(:check_ins_count),
-
total_flowers: enrollments.sum(:flowers_received_count)
-
}
-
end
-
-
# 费用计算方法
-
1
def calculate_leader_reward
-
return 0 if fee_type == 'free'
-
-
if fee_type == 'deposit'
-
fee_amount * (leader_reward_percentage / 100.0) * participants_count
-
else # paid
-
fee_amount * participants_count
-
end
-
end
-
-
1
def calculate_deposit_pool
-
return 0 if fee_type != 'deposit'
-
-
total_fees = fee_amount * participants_count
-
leader_reward = calculate_leader_reward
-
total_fees - leader_reward
-
end
-
-
# 验证活动是否满足审批条件(公开方法供Service使用)
-
1
def validate_event_for_approval
-
errors = []
-
-
# 检查基本信息
-
errors << "活动标题不能为空" if title.blank?
-
errors << "活动描述不能为空" if description.blank?
-
errors << "书籍名称不能为空" if book_name.blank?
-
-
# 检查日期设置
-
errors << "开始日期不能为空" if start_date.blank?
-
errors << "结束日期不能为空" if end_date.blank?
-
errors << "开始日期必须在今天之后" if start_date <= Date.today
-
-
# 检查人数设置
-
errors << "最大参与人数必须大于0" if max_participants.nil? || max_participants <= 0
-
errors << "最小参与人数不能大于最大参与人数" if min_participants > max_participants
-
-
# 检查费用设置(如果是收费活动)
-
if fee_type != 'free'
-
errors << "收费活动必须设置费用金额" if fee_amount.nil? || fee_amount <= 0
-
errors << "收费活动必须设置领读人奖励比例" if leader_reward_percentage.nil?
-
end
-
-
# 检查阅读计划
-
if reading_schedules.empty?
-
errors << "必须设置阅读计划"
-
end
-
-
# 检查特定活动模式的特殊要求
-
case activity_mode
-
when 'video_conference'
-
errors << "视频会议活动必须设置会议链接" if meeting_link.blank?
-
when 'offline_meeting'
-
errors << "线下活动必须设置活动地点" if location.blank?
-
end
-
-
{
-
valid: errors.empty?,
-
errors: errors
-
}
-
end
-
-
# 小红花统计
-
1
def flowers_count
-
Flower.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: id })
-
.count
-
end
-
-
1
def flowers_given_count
-
Flower.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: id })
-
.count
-
end
-
-
# 小红花配额
-
1
has_many :flower_quotas, dependent: :destroy
-
-
# 小红花证书
-
1
has_many :flower_certificates, dependent: :destroy
-
-
# 获取活动中的小红花排行榜(前三名)
-
1
def flower_top_three
-
flower_stats = Flower.joins(:recipient)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: id })
-
.group('recipients.id')
-
.sum(:amount)
-
-
flower_stats.sort_by { |user_id, flowers| -flowers }
-
.first(3)
-
.map.with_index(1) { |(user_id, flowers), index| [User.find(user_id), flowers, index] }
-
end
-
-
# 检查用户是否有剩余配额
-
1
def user_has_remaining_flower_quota?(user)
-
return false unless participants.include?(user)
-
-
quota = FlowerQuota.get_or_create_quota(user, self)
-
quota.can_give_flower?
-
end
-
-
# 活动结束时自动生成证书
-
1
def generate_flower_certificates_if_completed
-
return unless status == 'completed'
-
-
FlowerCertificate.generate_top_three_certificates(self)
-
end
-
-
# 检查用户是否在活动中有剩余小红花配额
-
1
def user_has_remaining_flower_quota?(user)
-
return false unless participants.include?(user)
-
-
quota = FlowerQuota.get_or_create_quota(user, self)
-
quota.can_give_flower?
-
end
-
-
# 获取用户在活动中的配额信息
-
1
def user_flower_quota_info(user)
-
return nil unless participants.include?(user)
-
-
FlowerIncentiveService.get_user_quota_info(user, self)
-
end
-
-
# 获取活动的小红花激励统计
-
1
def flower_incentive_statistics
-
return { error: '活动未结束' } unless status == 'completed'
-
-
certificates = FlowerCertificate.for_event(self).ranked
-
total_flowers_given = Flower.joins(:recipient)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: id })
-
.sum(:amount)
-
-
{
-
event: title,
-
status: status,
-
total_participants: participants.count,
-
total_flowers_given: total_flowers_given,
-
certificates_generated: certificates.count,
-
top_three_winners: certificates.map do |cert|
-
{
-
rank: cert.rank_display,
-
user: cert.user.as_json_for_api,
-
total_flowers: cert.total_flowers,
-
honor_level: cert.honor_level,
-
certificate_id: cert.certificate_id
-
}
-
end,
-
generated_at: certificates.first&.created_at
-
}
-
end
-
-
# 活动结束时生成小红花总结和证书
-
1
def finalize_flower_incentives
-
return { error: '活动未结束' } unless status == 'completed'
-
return { error: '活动没有参与者' } if participants.empty?
-
-
# 生成证书
-
result = FlowerIncentiveService.finalize_event_flower_certificates(self)
-
-
# 生成活动总结
-
summary = {
-
event: title,
-
duration: "#{start_date} 至 #{end_date}",
-
participants_count: participants.count,
-
certificates_generated: result[:certificates]&.count || 0,
-
total_flowers_given: flowers_count,
-
top_three: result[:certificates]&.map do |cert|
-
{
-
rank: cert[:rank_display],
-
user: cert[:user],
-
total_flowers: cert[:total_flowers]
-
}
-
end
-
}
-
-
{
-
success: true,
-
summary: summary,
-
certificates: result[:certificates]
-
}
-
end
-
-
# API响应格式化
-
1
def as_json_for_api(options = {})
-
base_data = {
-
id: id,
-
title: title,
-
book_name: book_name,
-
book_cover_url: book_cover_url,
-
description: description,
-
start_date: start_date,
-
end_date: end_date,
-
max_participants: max_participants,
-
min_participants: min_participants,
-
fee_type: fee_type,
-
fee_amount: fee_amount,
-
leader_reward_percentage: leader_reward_percentage,
-
completion_standard: completion_standard,
-
activity_mode: activity_mode,
-
weekend_rest: weekend_rest,
-
leader_assignment_type: leader_assignment_type,
-
status: status,
-
approval_status: approval_status,
-
created_at: created_at,
-
updated_at: updated_at
-
}
-
-
# 可选包含关联数据
-
if options[:include_leader]
-
base_data[:leader] = leader&.as_json_for_api
-
end
-
-
if options[:include_participants]
-
base_data[:participants] = participants.map(&:as_json_for_api)
-
end
-
-
if options[:include_statistics]
-
base_data[:statistics] = completion_statistics
-
base_data[:enrollment_statistics] = enrollment_statistics
-
end
-
-
if options[:include_schedules]
-
base_data[:reading_schedules] = reading_schedules.map do |schedule|
-
{
-
id: schedule.id,
-
day_number: schedule.day_number,
-
date: schedule.date,
-
reading_progress: schedule.reading_progress
-
}
-
end
-
end
-
-
base_data
-
end
-
-
1
private
-
-
1
def calculate_enrollment_rate
-
return 0 if max_participants == 0
-
(event_enrollments.enrolled.count.to_f / max_participants * 100).round(2)
-
end
-
-
1
def calculate_overall_completion_rate(enrollments)
-
return 0 if enrollments.empty?
-
(enrollments.average(:completion_rate) || 0).round(2)
-
end
-
-
1
def generate_reading_schedules
-
return unless start_date && end_date
-
-
(start_date..end_date).each_with_index do |date, index|
-
next if weekend_rest && (date.saturday? || date.sunday?)
-
-
reading_schedules.create!(
-
day_number: index + 1,
-
date: date,
-
reading_pages: nil, # 可以根据需要设置默认阅读页数
-
reading_content: nil
-
)
-
end
-
end
-
-
1
def generate_completion_certificates
-
event_enrollments.where(status: 'completed').each do |enrollment|
-
next unless enrollment.is_completed?
-
-
# 生成完成证书
-
ParticipationCertificate.generate_completion_certificate(enrollment)
-
-
# 检查小红花排名并生成相应证书
-
flower_rank = get_flower_rank(enrollment)
-
if flower_rank && flower_rank <= 3
-
ParticipationCertificate.generate_flower_certificate(enrollment, flower_rank)
-
end
-
end
-
end
-
-
1
def get_flower_rank(enrollment)
-
return 0 if enrollment.flowers_received_count == 0
-
-
rankings = event_enrollments
-
.where('flowers_received_count > 0')
-
.order(flowers_received_count: :desc)
-
.pluck(:id)
-
-
rankings.index(enrollment.id) + 1
-
end
-
-
1
private
-
-
1
def leader_flower_grace_period
-
leader_permission_window[:flower_give_days_after]
-
end
-
-
# 验证方法
-
1
def end_date_after_start_date
-
63
return if end_date.blank? || start_date.blank?
-
-
61
if end_date < start_date
-
1
errors.add(:end_date, "必须在开始日期之后")
-
end
-
end
-
-
1
def enrollment_deadline_before_start_date
-
return if enrollment_deadline.blank? || start_date.blank?
-
-
if enrollment_deadline > start_date.to_time
-
errors.add(:enrollment_deadline, "必须在活动开始日期之前")
-
end
-
end
-
-
1
def min_participants_not_greater_than_max
-
63
return if min_participants.blank? || max_participants.blank?
-
-
63
if min_participants > max_participants
-
2
errors.add(:min_participants, "不能大于最大参与人数")
-
end
-
end
-
-
1
def can_be_enrolling?
-
start_date > Date.current && approval_status == 'approved'
-
end
-
-
1
def generate_event_summary
-
# 这里可以实现活动总结的生成逻辑
-
# 比如统计小红花排名、完成率等
-
puts "活动【#{title}】已完成!"
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: reading_schedules
-
#
-
# id :integer not null, primary key
-
# reading_event_id :integer not null
-
# day_number :integer not null
-
# date :date not null
-
# reading_progress :string not null
-
# daily_leader_id :integer
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# index_reading_schedules_on_date (date)
-
# index_reading_schedules_on_daily_leader_id (daily_leader_id)
-
# index_reading_schedules_on_reading_event_id (reading_event_id)
-
# index_reading_schedules_on_reading_event_id_and_day_number (reading_event_id, day_number) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (daily_leader_id => users.id)
-
# fk_rails_... (reading_event_id => reading_events.id)
-
#
-
-
1
class ReadingSchedule < ApplicationRecord
-
# 关联关系
-
1
belongs_to :reading_event
-
1
belongs_to :daily_leader, class_name: 'User', optional: true
-
1
has_many :check_ins, dependent: :destroy
-
1
has_one :daily_leading, dependent: :destroy
-
1
has_many :flowers, dependent: :destroy
-
-
# 验证规则
-
1
validates :day_number, presence: true, numericality: { greater_than: 0 }
-
1
validates :reading_progress, presence: true, length: { maximum: 200 }
-
1
validates :date, presence: true
-
1
validates_uniqueness_of :day_number, scope: :reading_event_id
-
1
validate :date_within_event_period
-
1
validate :leader_must_be_event_participant, if: :daily_leader_id?
-
-
# 作用域
-
1
scope :today, -> { where(date: Date.current) }
-
1
scope :past, -> { where('date < ?', Date.current) }
-
1
scope :future, -> { where('date > ?', Date.current) }
-
1
scope :with_leader, -> { where.not(daily_leader_id: nil) }
-
1
scope :without_leader, -> { where(daily_leader_id: nil) }
-
1
scope :with_leading_content, -> { joins(:daily_leading) }
-
1
scope :chronological, -> { order(:day_number) }
-
1
scope :by_date, ->(direction = :asc) { order(date: direction) }
-
-
# 委托方法
-
1
delegate :title, :activity_mode, :in_progress?, to: :reading_event, prefix: true
-
-
# 状态方法
-
1
def today?
-
date == Date.current
-
end
-
-
1
def past?
-
date < Date.current
-
end
-
-
1
def future?
-
date > Date.current
-
end
-
-
1
def current_day?
-
reading_event.in_progress? && (date == Date.current || (date < Date.current && !completed?))
-
end
-
-
1
def can_assign_leader?
-
daily_leader_id.blank? && (future? || current_day?)
-
end
-
-
1
def can_publish_leading_content?
-
# 领读人权限窗口:前一天可以发布内容
-
return false unless daily_leader.present?
-
-
permission_start = date - 1.day
-
permission_end = date
-
-
Date.current.between?(permission_start, permission_end)
-
end
-
-
1
def can_give_flowers?
-
# 小红花发放权限窗口:当天和后一天
-
return false unless check_ins.any?
-
-
permission_start = date
-
permission_end = date + 1.day
-
-
Date.current.between?(permission_start, permission_end)
-
end
-
-
1
def has_leading_content?
-
daily_leading.present?
-
end
-
-
1
def has_check_ins?
-
check_ins.exists?
-
end
-
-
1
def has_flowers?
-
flowers.exists?
-
end
-
-
1
def completed?
-
return true if past? && has_check_ins?
-
return true if reading_event.completed?
-
false
-
end
-
-
# 领读人分配方法
-
1
def assign_leader!(user)
-
return false unless can_assign_leader?
-
return false unless reading_event.participants.include?(user)
-
-
transaction do
-
update!(daily_leader: user)
-
notify_leader_assignment(user)
-
end
-
true
-
end
-
-
1
def remove_leader!
-
return false unless daily_leader.present?
-
-
transaction do
-
update!(daily_leader: nil)
-
# 删除相关的领读内容
-
daily_leading&.destroy
-
end
-
true
-
end
-
-
# 统计方法
-
1
def participation_statistics
-
{
-
check_ins_count: check_ins.count,
-
flowers_count: flowers.count,
-
unique_participants: check_ins.distinct.count(:user_id),
-
average_word_count: check_ins.average(:word_count)&.round(2) || 0
-
}
-
end
-
-
1
def leading_content_status
-
return 'no_leader' if daily_leader.blank?
-
return 'content_published' if has_leading_content?
-
return 'content_pending' if can_publish_leading_content?
-
'content_overdue'
-
end
-
-
1
def flower_giving_status
-
return 'no_check_ins' unless has_check_ins?
-
return 'flowers_given' if has_flowers?
-
return 'flowers_pending' if can_give_flowers?
-
'flowers_overdue'
-
end
-
-
# 检查是否需要小组长补位
-
1
def needs_backup?
-
return false unless reading_event.in_progress?
-
-
# 检查领读内容是否缺失
-
content_missing = daily_leader.present? && !has_leading_content? && !can_publish_leading_content?
-
-
# 检查小红花是否缺失
-
flowers_missing = has_check_ins? && !has_flowers? && !can_give_flowers?
-
-
content_missing || flowers_missing
-
end
-
-
# 获取补位权限
-
1
def backup_permissions
-
return {} unless reading_event.in_progress?
-
-
{
-
can_publish_content: reading_event.current_leader?(reading_event.leader),
-
can_give_flowers: reading_event.current_leader?(reading_event.leader),
-
content_deadline: date,
-
flowers_deadline: date + 1.day
-
}
-
end
-
-
# 通知方法
-
1
def notify_leader_assignment(leader)
-
# 发送领读人分配通知
-
LeaderAssignmentService.notify_assignment(self, leader)
-
end
-
-
1
def notify_leading_content_published
-
return unless daily_leader.present?
-
-
# 发送领读内容发布通知
-
LeaderAssignmentService.notify_content_published(self)
-
end
-
-
1
def notify_check_in_submitted(check_in)
-
# 发送打卡提交通知给领读人和小组长
-
CheckInNotificationService.notify_submitted(check_in)
-
end
-
-
1
def notify_flower_given(flower)
-
# 发送小红花发放通知
-
FlowerNotificationService.notify_given(flower)
-
end
-
-
1
private
-
-
# 验证方法
-
1
def date_within_event_period
-
18
return unless date && reading_event
-
-
18
if date < reading_event.start_date || date > reading_event.end_date
-
errors.add(:date, "必须在活动时间范围内")
-
end
-
end
-
-
1
def leader_must_be_event_participant
-
7
return unless daily_leader_id && reading_event
-
-
7
unless reading_event.participants.include?(daily_leader)
-
7
errors.add(:daily_leader, "必须是活动的参与者")
-
end
-
end
-
end
-
class ShareAction < ApplicationRecord
-
# 关联
-
belongs_to :user, optional: true
-
-
# 验证
-
validates :share_type, :resource_id, :platform, presence: true
-
-
# 枚举
-
enum :share_type, {
-
daily_leaderboard: 'daily_leaderboard', # 每日排行榜
-
final_leaderboard: 'final_leaderboard', # 最终排行榜
-
certificate: 'certificate', # 证书分享
-
user_achievement: 'user_achievement' # 用户成就
-
}
-
-
enum :platform, {
-
wechat: 'wechat', # 微信
-
weibo: 'weibo', # 微博
-
qq: 'qq', # QQ
-
copy_link: 'copy_link' # 复制链接
-
}
-
-
# 作用域
-
scope :for_share_type, ->(type) { where(share_type: type) }
-
scope :for_platform, ->(platform) { where(platform: platform) }
-
scope :for_user, ->(user) { where(user: user) }
-
scope :recent, -> { order(shared_at: :desc) }
-
scope :today, -> { where(shared_at: Date.current.beginning_of_day..Date.current.end_of_day) }
-
-
# 回调
-
before_validation :set_shared_at, on: :create
-
-
# 实例方法
-
-
# 获取分享的资源对象
-
def resource
-
case share_type
-
when 'daily_leaderboard'
-
DailyFlowerStat.find_by(id: resource_id)
-
when 'final_leaderboard'
-
FlowerCertificate.for_event(ReadingEvent.find_by(id: resource_id))
-
when 'certificate'
-
FlowerCertificate.find_by(certificate_id: resource_id)
-
when 'user_achievement'
-
{ user_id: user_id, event_id: resource_id }
-
end
-
end
-
-
# 获取分享的显示名称
-
def share_type_display
-
case share_type
-
when 'daily_leaderboard'
-
'每日排行榜'
-
when 'final_leaderboard'
-
'最终排行榜'
-
when 'certificate'
-
'证书分享'
-
when 'user_achievement'
-
'个人成就'
-
else
-
share_type
-
end
-
end
-
-
# 获取平台显示名称
-
def platform_display
-
case platform
-
when 'wechat'
-
'微信'
-
when 'weibo'
-
'微博'
-
when 'qq'
-
'QQ'
-
when 'copy_link'
-
'复制链接'
-
else
-
platform
-
end
-
end
-
-
# 检查是否为今日分享
-
def shared_today?
-
shared_at.to_date == Date.current
-
end
-
-
# API响应格式
-
def as_json_for_api
-
{
-
id: id,
-
share_type: share_type_display,
-
platform: platform_display,
-
resource_id: resource_id,
-
user: user&.as_json_for_api,
-
shared_at: shared_at,
-
shared_today: shared_today?,
-
ip_address: ip_address
-
}
-
end
-
-
# 类方法
-
-
# 获取分享统计
-
def self.share_statistics(days: 7)
-
start_date = days.days.ago.to_date
-
-
stats = where('shared_at >= ?', start_date)
-
.group(:share_type, :platform)
-
.count
-
-
{
-
period: "#{start_date} 至 #{Date.current}",
-
total_shares: stats.values.sum,
-
share_type_breakdown: stats.group_by { |(type, _), _| type }
-
.transform_values { |items| items.values.sum },
-
platform_breakdown: stats.group_by { |(_, platform), _| platform }
-
.transform_values(&:sum),
-
detailed_stats: stats
-
}
-
end
-
-
# 获取用户的分享历史
-
def self.user_share_history(user, limit: 20)
-
for_user(user)
-
.recent
-
.limit(limit)
-
.includes(:user)
-
end
-
-
# 获取热门分享内容
-
def self.popular_shares(days: 7, limit: 10)
-
start_date = days.days.ago.to_date
-
-
joins(:user)
-
.where('shared_at >= ?', start_date)
-
.group(:share_type, :resource_id)
-
.order('COUNT(*) DESC')
-
.limit(limit)
-
.count
-
end
-
-
# 记录分享行为(便捷方法)
-
def self.record_share(share_type:, resource_id:, platform:, user: nil, ip_address: nil, user_agent: nil)
-
create!(
-
share_type: share_type,
-
resource_id: resource_id,
-
platform: platform,
-
user: user,
-
ip_address: ip_address,
-
user_agent: user_agent,
-
shared_at: Time.current
-
)
-
rescue => e
-
Rails.logger.error "记录分享行为失败: #{e.message}"
-
nil
-
end
-
-
private
-
-
def set_shared_at
-
self.shared_at ||= Time.current
-
end
-
end
-
1
require 'jwt'
-
-
1
class User < ApplicationRecord
-
# 关联
-
1
has_many :created_events, class_name: "ReadingEvent", foreign_key: "leader_id", dependent: :destroy
-
1
has_many :enrollments, class_name: "EventEnrollment", dependent: :destroy
-
1
has_many :event_enrollments, class_name: "EventEnrollment", dependent: :destroy # 为了兼容分析系统
-
1
has_many :reading_events, through: :enrollments
-
1
has_many :posts, dependent: :destroy
-
1
has_many :check_ins, dependent: :destroy
-
1
has_many :comments, dependent: :destroy
-
-
# 小红花相关关联
-
1
has_many :received_flowers, class_name: "Flower", foreign_key: "recipient_id", dependent: :destroy
-
1
has_many :given_flowers, class_name: "Flower", foreign_key: "giver_id", dependent: :destroy
-
1
has_many :flowers, foreign_key: "giver_id", dependent: :destroy # 为了兼容分析系统
-
1
has_many :flower_quotas, dependent: :destroy
-
1
has_many :flower_certificates, dependent: :destroy
-
-
# 通知相关关联
-
1
has_many :received_notifications, class_name: "Notification", foreign_key: "recipient_id", dependent: :destroy
-
1
has_many :sent_notifications, class_name: "Notification", foreign_key: "actor_id", dependent: :destroy
-
-
# 验证
-
1
validates :wx_openid, presence: true, uniqueness: true
-
1
validates :wx_unionid, uniqueness: true, allow_nil: true
-
1
validates :nickname, presence: true, length: { minimum: 1, maximum: 50 }, allow_blank: false
-
-
# 枚举:用户角色(暂时注释掉以解决API问题)
-
# enum role: %w[user admin root], default: 'user'
-
-
# 生成 JWT token
-
1
def generate_jwt_token
-
payload = {
-
2
user_id: id,
-
wx_openid: wx_openid,
-
role: role_as_string, # 使用字符串角色名
-
exp: 30.days.from_now.to_i,
-
iat: Time.current.to_i, # 签发时间
-
type: 'access' # token类型
-
}
-
2
JWT.encode(payload, Rails.application.credentials.jwt_secret_key || "dev_secret_key")
-
end
-
-
# 生成refresh token(长期有效)
-
1
def generate_refresh_token
-
payload = {
-
user_id: id,
-
wx_openid: wx_openid,
-
type: 'refresh',
-
exp: 90.days.from_now.to_i, # 90天有效期
-
iat: Time.current.to_i
-
}
-
JWT.encode(payload, Rails.application.credentials.jwt_secret_key || "dev_secret_key")
-
end
-
-
# 解析refresh token
-
1
def self.decode_refresh_token(token)
-
begin
-
decoded = JWT.decode(token, Rails.application.credentials.jwt_secret_key || "dev_secret_key")[0]
-
return nil unless decoded['type'] == 'refresh'
-
HashWithIndifferentAccess.new(decoded)
-
rescue JWT::DecodeError => e
-
Rails.logger.warn "Refresh token解码失败: #{e.message}"
-
nil
-
end
-
end
-
-
# 使用refresh token生成新的access token
-
1
def self.refresh_access_token(refresh_token)
-
decoded = decode_refresh_token(refresh_token)
-
return nil unless decoded
-
-
user = User.find_by(id: decoded['user_id'])
-
return nil unless user
-
-
# 验证openid是否匹配
-
return nil unless user.wx_openid == decoded['wx_openid']
-
-
# 生成新的access token
-
new_access_token = user.generate_jwt_token
-
-
{
-
access_token: new_access_token,
-
refresh_token: refresh_token, # refresh token可以继续使用
-
user: user.as_json_for_api
-
}
-
end
-
-
# 解析 JWT token
-
1
def self.decode_jwt_token(token)
-
begin
-
# 移除 Bearer 前缀
-
3
token = token.gsub('Bearer ', '') if token&.start_with?('Bearer ')
-
-
3
Rails.logger.info "JWT Token: #{token[0..50]}..." if token
-
3
secret = Rails.application.credentials.jwt_secret_key || "dev_secret_key"
-
3
Rails.logger.info "JWT Secret: #{secret[0..10]}..." if secret
-
3
decoded = JWT.decode(token, secret)[0]
-
1
Rails.logger.info "JWT Decoded: #{decoded.inspect}"
-
1
HashWithIndifferentAccess.new(decoded)
-
rescue JWT::DecodeError => e
-
2
Rails.logger.error "JWT Decode Error: #{e.message}"
-
2
nil
-
rescue => e
-
Rails.logger.error "JWT Unexpected Error: #{e.message}"
-
nil
-
end
-
end
-
-
# 简化的角色权限检查方法
-
1
def user?
-
5
role.to_s == 'user' || role.to_s == '0'
-
end
-
-
1
def participant?
-
role.to_s == 'user' || role.to_s == '0' # 同义词,与user相同
-
end
-
-
1
def admin?
-
34
role.to_s == 'admin' || role.to_s == '1'
-
end
-
-
1
def root?
-
33
role.to_s == 'root' || role.to_s == '2'
-
end
-
-
1
def any_admin?
-
15
admin? || root?
-
end
-
-
# 管理员权限检查
-
1
def can_manage_users?
-
6
root?
-
end
-
-
1
def can_approve_events?
-
6
admin? || root?
-
end
-
-
1
def can_view_approval_queue?
-
admin? || root?
-
end
-
-
1
def can_view_admin_panel?
-
6
admin? || root?
-
end
-
-
1
def can_manage_system?
-
6
root?
-
end
-
-
# 基础用户权限
-
1
def can_create_posts?
-
5
true # 所有用户都可以发帖
-
end
-
-
1
def can_comment?
-
4
true # 所有用户都可以评论
-
end
-
-
1
def can_join_events?
-
4
true # 所有用户都可以报名活动
-
end
-
-
# 活动相关权限检查(基于 Enrollment,不是角色)
-
1
def is_event_leader?(event)
-
3
return false unless event
-
2
event.leader_id == id
-
end
-
-
1
def is_daily_leader?(event, schedule)
-
return false unless event && schedule
-
return false unless schedule.reading_event_id == event.id
-
schedule.daily_leader_id == id
-
end
-
-
# 角色提升方法
-
1
def promote_to_admin!
-
update!(role: 1) if user? || participant? # 1 represents admin in integer form
-
end
-
-
1
def demote_to_user!
-
1
update!(role: 0) # 0 represents user in integer form
-
end
-
-
# 获取角色显示名称
-
1
def role_display_name
-
9
case role.to_s
-
when 'user', '0'
-
6
'用户'
-
when 'admin', '1'
-
1
'管理员'
-
when 'root', '2'
-
1
'超级管理员'
-
else
-
1
'未知角色'
-
end
-
end
-
-
# 获取角色字符串名称(用于JWT token)
-
1
def role_as_string
-
3
case role.to_s
-
when 'user', '0'
-
3
'user'
-
when 'admin', '1'
-
'admin'
-
when 'root', '2'
-
'root'
-
else
-
'user' # 默认为user
-
end
-
end
-
-
# 检查用户是否有特定权限
-
1
def has_permission?(permission)
-
16
case permission
-
when :approve_events
-
3
can_approve_events?
-
when :manage_users
-
3
can_manage_users?
-
when :view_admin_panel
-
3
can_view_admin_panel?
-
when :manage_system
-
3
can_manage_system?
-
when :create_posts
-
2
can_create_posts?
-
when :comment
-
1
can_comment?
-
when :join_events
-
1
can_join_events?
-
else
-
false
-
end
-
end
-
-
# 用于API响应的用户信息格式化
-
1
def as_json_for_api
-
{
-
id: id,
-
nickname: nickname,
-
wx_openid: wx_openid,
-
avatar_url: avatar_url,
-
phone: phone,
-
role: role_as_string
-
}
-
end
-
end
-
# frozen_string_literal: true
-
-
# UserActivity - 用户活动模型
-
# 记录用户的各种行为和活动轨迹
-
class UserActivity < ApplicationRecord
-
belongs_to :user
-
-
validates :user, presence: true
-
validates :action_type, presence: true
-
validates :details, presence: true
-
-
# 活动类型枚举
-
enum :action_type, {
-
# 内容相关
-
post_created: 'post_created',
-
post_updated: 'post_updated',
-
post_deleted: 'post_deleted',
-
comment_created: 'comment_created',
-
comment_updated: 'comment_updated',
-
comment_deleted: 'comment_deleted',
-
like_given: 'like_given',
-
like_removed: 'like_removed',
-
-
# 活动相关
-
event_joined: 'event_joined',
-
event_left: 'event_left',
-
event_completed: 'event_completed',
-
check_in_created: 'check_in_created',
-
flower_given: 'flower_given',
-
flower_received: 'flower_received',
-
-
# 社交相关
-
profile_viewed: 'profile_viewed',
-
user_followed: 'user_followed',
-
user_unfollowed: 'user_unfollowed',
-
-
# 系统相关
-
login: 'login',
-
logout: 'logout',
-
password_changed: 'password_changed',
-
profile_updated: 'profile_updated',
-
settings_changed: 'settings_changed',
-
-
# 页面浏览
-
page_view: 'page_view',
-
api_call: 'api_call'
-
}
-
-
# 作用域
-
scope :recent, -> { order(created_at: :desc) }
-
scope :today, -> { where(created_at: Date.current.all_day) }
-
scope :this_week, -> { where(created_at: Date.current.beginning_of_week..Date.current.end_of_week) }
-
scope :this_month, -> { where(created_at: Date.current.beginning_of_month..Date.current.end_of_month) }
-
scope :by_action_type, ->(type) { where(action_type: type) }
-
scope :by_user, ->(user) { where(user: user) }
-
-
# 类方法:记录用户活动
-
def self.track(user:, action_type:, details: {})
-
return unless user
-
-
create!(
-
user: user,
-
action_type: action_type,
-
details: details.merge(
-
timestamp: Time.current.iso8601,
-
ip: details[:ip] || '0.0.0.0',
-
user_agent: details[:user_agent] || 'Unknown'
-
)
-
)
-
rescue => e
-
Rails.logger.error "Failed to track user activity: #{e.message}"
-
end
-
-
# 类方法:获取用户活动统计
-
def self.activity_stats(user, period = :week)
-
case period
-
when :day
-
start_time = Date.current.beginning_of_day
-
end_time = Date.current.end_of_day
-
when :week
-
start_time = Date.current.beginning_of_week
-
end_time = Date.current.end_of_week
-
when :month
-
start_time = Date.current.beginning_of_month
-
end_time = Date.current.end_of_month
-
else
-
start_time = 30.days.ago
-
end_time = Time.current
-
end
-
-
activities = where(user: user)
-
.where(created_at: start_time..end_time)
-
-
{
-
total_activities: activities.count,
-
action_breakdown: activities.group(:action_type).count,
-
most_active_day: find_most_active_day(activities),
-
average_daily_activities: calculate_daily_average(activities, period)
-
}
-
end
-
-
# 类方法:获取用户最近活动
-
def self.recent_activities(user, limit = 10)
-
where(user: user)
-
.recent
-
.limit(limit)
-
end
-
-
# 类方法:清理旧活动记录
-
def self.cleanup_old_activities(days_to_keep = 90)
-
cutoff_date = days_to_keep.days.ago
-
-
where('created_at < ?', cutoff_date).delete_all
-
end
-
-
# 类方法:获取活动趋势
-
def self.activity_trend(user, days = 7)
-
end_date = Date.current
-
start_date = days.days.ago.to_date
-
-
activities = where(user: user)
-
.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
-
.group_by_day(:created_at)
-
.count
-
-
trend_data = []
-
(start_date..end_date).each do |date|
-
trend_data << {
-
date: date.iso8601,
-
count: activities[date] || 0
-
}
-
end
-
-
trend_data
-
end
-
-
# 类方法:获取用户活跃度评分
-
def self.activity_score(user)
-
# 基于最近30天的活动计算活跃度评分
-
cutoff_date = 30.days.ago
-
recent_activities = where(user: user)
-
.where('created_at > ?', cutoff_date)
-
-
score = 0
-
-
# 内容创作得分
-
content_actions = %w[post_created comment_created]
-
content_score = recent_activities.where(action_type: content_actions).count * 10
-
score += content_score
-
-
# 社交互动得分
-
social_actions = %w[like_given flower_given event_joined]
-
social_score = recent_activities.where(action_type: social_actions).count * 5
-
score += social_score
-
-
# 登录活跃度得分
-
login_actions = %w[login page_view api_call]
-
login_score = [recent_activities.where(action_type: login_actions).count, 100].min
-
score += login_score
-
-
# 时间衰减因子(越近的活动权重越高)
-
time_decay_factor = calculate_time_decay_factor(recent_activities)
-
score = (score * time_decay_factor).round
-
-
{
-
score: score,
-
level: activity_level(score),
-
content_score: content_score,
-
social_score: social_score,
-
login_score: login_score,
-
time_decay_factor: time_decay_factor
-
}
-
end
-
-
# 实例方法:格式化活动描述
-
def formatted_description
-
case action_type
-
when 'post_created'
-
"发布了新帖子「#{details['post_title']}」"
-
when 'comment_created'
-
"评论了帖子「#{details['post_title']}」"
-
when 'like_given'
-
"点赞了#{details['target_type']}「#{details['target_title']}」"
-
when 'event_joined'
-
"参加了活动「#{details['event_title']}」"
-
when 'flower_given'
-
"给#{details['recipient_name']}送了一朵小红花"
-
when 'login'
-
"登录了系统"
-
when 'page_view'
-
"浏览了#{details['path']}页面"
-
else
-
action_type.humanize
-
end
-
end
-
-
# 实例方法:获取活动图标
-
def icon
-
case action_type
-
when 'post_created', 'post_updated'
-
'edit'
-
when 'comment_created', 'comment_updated'
-
'comment'
-
when 'like_given'
-
'heart'
-
when 'event_joined', 'event_completed'
-
'calendar'
-
when 'flower_given'
-
'flower'
-
when 'login'
-
'log-in'
-
when 'page_view'
-
'eye'
-
else
-
'activity'
-
end
-
end
-
-
# 实例方法:获取活动颜色
-
def color
-
case action_type
-
when 'post_created', 'comment_created', 'like_given'
-
'blue'
-
when 'event_joined', 'event_completed'
-
'green'
-
when 'flower_given'
-
'red'
-
when 'login'
-
'gray'
-
else
-
'default'
-
end
-
end
-
-
# 实例方法:是否为重要活动
-
def important?
-
%w[post_created event_joined flower_given].include?(action_type)
-
end
-
-
# 实例方法:获取活动链接
-
def activity_link
-
case action_type
-
when 'post_created', 'post_updated'
-
"/posts/#{details['post_id']}" if details['post_id']
-
when 'comment_created'
-
"/posts/#{details['post_id']}#comment-#{details['comment_id']}" if details['post_id'] && details['comment_id']
-
when 'event_joined', 'event_completed'
-
"/events/#{details['event_id']}" if details['event_id']
-
when 'profile_viewed'
-
"/users/#{details['profile_user_id']}" if details['profile_user_id']
-
else
-
nil
-
end
-
end
-
-
private
-
-
def self.find_most_active_day(activities)
-
day_counts = activities.group_by_day(:created_at).count
-
return nil if day_counts.empty?
-
-
most_active_date = day_counts.max_by { |_, count| count }&.first
-
return nil unless most_active_date
-
-
{
-
date: most_active_date.iso8601,
-
count: day_counts[most_active_date]
-
}
-
end
-
-
def self.calculate_daily_average(activities, period)
-
case period
-
when :day
-
activities.count.to_f
-
when :week
-
(activities.count / 7.0).round(2)
-
when :month
-
(activities.count / 30.0).round(2)
-
else
-
(activities.count / 7.0).round(2)
-
end
-
end
-
-
def self.calculate_time_decay_factor(activities)
-
return 0.0 if activities.empty?
-
-
# 计算时间衰减因子,越近的活动权重越高
-
total_weight = 0.0
-
total_activities = activities.count
-
-
activities.each do |activity|
-
days_ago = (Time.current - activity.created_at) / 1.day
-
weight = Math.exp(-days_ago / 7.0) # 7天衰减常数
-
total_weight += weight
-
end
-
-
(total_weight / total_activities).round(3)
-
end
-
-
def self.activity_level(score)
-
case score
-
when 0..10
-
'inactive'
-
when 11..50
-
'low'
-
when 51..150
-
'moderate'
-
when 151..300
-
'high'
-
else
-
'very_high'
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ActivityApprovalWorkflowService - 活动审批工作流服务
-
# 负责活动审批的完整业务流程,包括多级审批、条件检查、通知等
-
class ActivityApprovalWorkflowService < ApplicationService
-
attr_reader :event, :admin_user, :action, :approval_options, :workflow_type
-
-
def initialize(event:, admin_user:, action:, workflow_type: :standard, approval_options: {})
-
super()
-
@event = event
-
@admin_user = admin_user
-
@action = action
-
@workflow_type = workflow_type
-
@approval_options = approval_options ? approval_options.with_indifferent_access : {}.with_indifferent_access
-
end
-
-
def call
-
handle_errors do
-
case action
-
when :submit_for_approval
-
submit_for_approval
-
when :approve
-
process_approval
-
when :reject
-
process_rejection
-
when :batch_approve
-
batch_approve_events
-
when :batch_reject
-
batch_reject_events
-
when :get_approval_queue
-
get_approval_queue
-
when :get_approval_statistics
-
get_approval_statistics
-
when :escalate
-
escalate_approval
-
else
-
failure!("不支持的审批操作: #{action}")
-
end
-
end
-
end
-
-
# 类方法:提交审批
-
def self.submit_for_approval!(event, workflow_type: :standard)
-
service = new(event: event, admin_user: event.leader, action: :submit_for_approval, workflow_type: workflow_type)
-
service.call
-
service
-
end
-
-
# 类方法:审批通过
-
def self.approve!(event, admin_user, reason: nil, notes: nil)
-
service = new(event: event, admin_user: admin_user, action: :approve,
-
approval_options: { reason: reason, notes: notes })
-
service.call
-
service
-
end
-
-
# 类方法:审批拒绝
-
def self.reject!(event, admin_user, reason, notes: nil)
-
service = new(event: event, admin_user: admin_user, action: :reject,
-
approval_options: { reason: reason, notes: notes })
-
service.call
-
service
-
end
-
-
# 类方法:批量审批
-
def self.batch_approve!(event_ids, admin_user, reason: nil)
-
service = new(event: nil, admin_user: admin_user, action: :batch_approve,
-
approval_options: { event_ids: event_ids, reason: reason })
-
service.call
-
service
-
end
-
-
# 类方法:批量拒绝
-
def self.batch_reject!(event_ids, admin_user, reason)
-
service = new(event: nil, admin_user: admin_user, action: :batch_reject,
-
approval_options: { event_ids: event_ids, reason: reason })
-
service.call
-
service
-
end
-
-
# 类方法:获取审批队列
-
def self.approval_queue(admin_user, filters = {})
-
service = new(event: nil, admin_user: admin_user, action: :get_approval_queue,
-
approval_options: filters)
-
service.call
-
service
-
end
-
-
# 类方法:获取审批统计
-
def self.approval_statistics(admin_user, date_range: nil)
-
service = new(event: nil, admin_user: admin_user, action: :get_approval_statistics,
-
approval_options: { date_range: date_range })
-
service.call
-
service
-
end
-
-
# 类方法:升级审批
-
def self.escalate!(event, admin_user, escalation_reason)
-
service = new(event: event, admin_user: admin_user, action: :escalate,
-
approval_options: { escalation_reason: escalation_reason })
-
service.call
-
service
-
end
-
-
private
-
-
# 提交审批申请
-
def submit_for_approval
-
# 检查是否可以提交审批
-
unless event.can_submit_for_approval?
-
return failure!("活动当前状态无法提交审批")
-
end
-
-
# 检查审批前置条件
-
validation_result = validate_event_for_approval
-
unless validation_result[:valid]
-
return failure!(validation_result[:errors].join(", "))
-
end
-
-
ActiveRecord::Base.transaction do
-
# 更新活动状态为待审批
-
event.update!(
-
status: :draft,
-
approval_status: :pending,
-
submitted_for_approval_at: Time.current
-
)
-
-
# 记录审批日志
-
create_approval_log(:submitted, event.leader, "提交审批申请")
-
-
# 发送通知给审批管理员(暂时注释,等待通知系统实现)
-
# send_approval_notifications
-
-
success!({
-
message: "活动已提交审批,请等待管理员审核",
-
event: event_approval_info,
-
approval_queue_position: get_approval_queue_position
-
})
-
end
-
rescue => e
-
failure!("提交审批失败: #{e.message}")
-
end
-
-
# 处理审批通过
-
def process_approval
-
# 检查审批权限
-
unless admin_user.can_approve_events?
-
return failure!("权限不足,无法审批活动")
-
end
-
-
# 检查活动状态
-
unless event.pending_approval?
-
return failure!("活动当前状态无法审批")
-
end
-
-
# 执行审批通过流程
-
ActiveRecord::Base.transaction do
-
# 更新活动状态
-
event.update!(
-
status: :enrolling,
-
approval_status: :approved,
-
approved_by_id: admin_user.id,
-
approved_at: Time.current,
-
approval_reason: @approval_options[:reason],
-
approval_notes: @approval_options[:notes]
-
)
-
-
# 记录审批日志
-
create_approval_log(:approved, admin_user, @approval_options[:reason])
-
-
# 发送审批通过通知
-
send_approval_decision_notification(:approved)
-
-
success!({
-
message: "活动审批通过",
-
event: event_approval_info,
-
approval_details: approval_decision_info
-
})
-
end
-
rescue => e
-
failure!("审批通过失败: #{e.message}")
-
end
-
-
# 处理审批拒绝
-
def process_rejection
-
# 检查审批权限
-
unless admin_user.can_approve_events?
-
return failure!("权限不足,无法审批活动")
-
end
-
-
# 检查拒绝理由
-
rejection_reason = @approval_options[:reason]
-
if rejection_reason.blank?
-
return failure!("请提供拒绝理由")
-
end
-
-
# 检查活动状态
-
unless event.pending_approval?
-
return failure!("活动当前状态无法审批")
-
end
-
-
ActiveRecord::Base.transaction do
-
# 更新活动状态
-
event.update!(
-
approval_status: :rejected,
-
approved_by_id: admin_user.id,
-
approved_at: Time.current,
-
rejection_reason: rejection_reason,
-
approval_notes: @approval_options[:notes]
-
)
-
-
# 记录审批日志
-
create_approval_log(:rejected, admin_user, rejection_reason)
-
-
# 发送审批拒绝通知
-
send_approval_decision_notification(:rejected)
-
-
success!({
-
message: "活动已拒绝",
-
event: event_approval_info,
-
rejection_details: {
-
reason: rejection_reason,
-
notes: @approval_options[:notes],
-
resubmission_allowed: event.can_resubmit_for_approval?
-
}
-
})
-
end
-
rescue => e
-
failure!("审批拒绝失败: #{e.message}")
-
end
-
-
# 批量审批通过
-
def batch_approve_events
-
unless admin_user.can_approve_events?
-
return failure!("权限不足,无法批量审批活动")
-
end
-
-
event_ids = @approval_options[:event_ids]
-
if event_ids.blank? || !event_ids.is_a?(Array)
-
return failure!("请提供有效的活动ID列表")
-
end
-
-
events = ReadingEvent.where(id: event_ids, approval_status: :pending)
-
if events.empty?
-
return failure!("没有找到待审批的活动")
-
end
-
-
approval_results = []
-
failed_count = 0
-
-
ActiveRecord::Base.transaction do
-
events.each do |event_item|
-
begin
-
event_item.update!(
-
status: :enrolling,
-
approval_status: :approved,
-
approved_by_id: admin_user.id,
-
approved_at: Time.current,
-
approval_reason: @approval_options[:reason]
-
)
-
-
create_approval_log(:approved, admin_user, @approval_options[:reason], event_item)
-
approval_results << { event_id: event_item.id, status: 'approved', success: true }
-
rescue => e
-
failed_count += 1
-
approval_results << { event_id: event_item.id, status: 'failed', error: e.message, success: false }
-
end
-
end
-
end
-
-
success!({
-
message: "批量审批完成,成功: #{events.count - failed_count},失败: #{failed_count}",
-
batch_results: approval_results,
-
summary: {
-
total: events.count,
-
successful: events.count - failed_count,
-
failed: failed_count
-
}
-
})
-
rescue => e
-
failure!("批量审批失败: #{e.message}")
-
end
-
-
# 批量审批拒绝
-
def batch_reject_events
-
unless admin_user.can_approve_events?
-
return failure!("权限不足,无法批量审批活动")
-
end
-
-
rejection_reason = @approval_options[:reason]
-
if rejection_reason.blank?
-
return failure!("请提供拒绝理由")
-
end
-
-
event_ids = @approval_options[:event_ids]
-
if event_ids.blank? || !event_ids.is_a?(Array)
-
return failure!("请提供有效的活动ID列表")
-
end
-
-
events = ReadingEvent.where(id: event_ids, approval_status: :pending)
-
if events.empty?
-
return failure!("没有找到待审批的活动")
-
end
-
-
rejection_results = []
-
failed_count = 0
-
-
ActiveRecord::Base.transaction do
-
events.each do |event_item|
-
begin
-
event_item.update!(
-
approval_status: :rejected,
-
approved_by_id: admin_user.id,
-
approved_at: Time.current,
-
rejection_reason: rejection_reason,
-
approval_notes: @approval_options[:notes]
-
)
-
-
create_approval_log(:rejected, admin_user, rejection_reason, event_item)
-
rejection_results << { event_id: event_item.id, status: 'rejected', success: true }
-
rescue => e
-
failed_count += 1
-
rejection_results << { event_id: event_item.id, status: 'failed', error: e.message, success: false }
-
end
-
end
-
end
-
-
success!({
-
message: "批量拒绝完成,成功: #{events.count - failed_count},失败: #{failed_count}",
-
batch_results: rejection_results,
-
summary: {
-
total: events.count,
-
successful: events.count - failed_count,
-
failed: failed_count
-
}
-
})
-
rescue => e
-
failure!("批量拒绝失败: #{e.message}")
-
end
-
-
# 获取审批队列
-
def get_approval_queue
-
# 检查查看权限
-
unless admin_user.can_approve_events? || admin_user.can_view_approval_queue?
-
return failure!("权限不足,无法查看审批队列")
-
end
-
-
filters = @approval_options
-
events = ReadingEvent.includes(:leader).where(approval_status: :pending)
-
-
# 应用过滤条件
-
events = apply_approval_queue_filters(events, filters)
-
-
# 排序
-
events = events.order(submitted_for_approval_at: :asc)
-
-
# 分页
-
page = filters[:page] || 1
-
per_page = filters[:per_page] || 20
-
total_count = events.count
-
paginated_events = events.limit(per_page).offset((page - 1) * per_page)
-
-
queue_data = paginated_events.map do |event_item|
-
event_approval_queue_info(event_item)
-
end
-
-
success!({
-
approval_queue: queue_data,
-
pagination: {
-
current_page: page,
-
per_page: per_page,
-
total_count: total_count,
-
total_pages: (total_count.to_f / per_page).ceil
-
},
-
filters_applied: filters,
-
queue_statistics: get_queue_statistics(events)
-
})
-
end
-
-
# 获取审批统计
-
def get_approval_statistics
-
unless admin_user.can_approve_events?
-
return failure!("权限不足,无法查看审批统计")
-
end
-
-
date_range = @approval_options[:date_range] || (Date.today - 30.days)..Date.today
-
-
stats = {
-
total_pending: ReadingEvent.where(approval_status: :pending).count,
-
total_approved: ReadingEvent.where(approval_status: :approved).count,
-
total_rejected: ReadingEvent.where(approval_status: :rejected).count,
-
-
# 期间统计
-
period_approved: ReadingEvent.where(approval_status: :approved, approved_at: date_range).count,
-
period_rejected: ReadingEvent.where(approval_status: :rejected, approved_at: date_range).count,
-
-
# 审批效率统计
-
average_approval_time: calculate_average_approval_time(date_range),
-
approval_rate: calculate_approval_rate(date_range),
-
-
# 管理员统计
-
admin_stats: get_admin_approval_stats(date_range),
-
-
# 活动类型统计
-
activity_mode_stats: get_activity_mode_approval_stats(date_range)
-
}
-
-
success!(stats)
-
end
-
-
# 升级审批
-
def escalate_approval
-
unless event.pending_approval?
-
return failure!("只有待审批的活动可以升级审批")
-
end
-
-
escalation_reason = @approval_options[:escalation_reason]
-
if escalation_reason.blank?
-
return failure!("请提供升级理由")
-
end
-
-
ActiveRecord::Base.transaction do
-
# 记录升级日志
-
create_approval_log(:escalated, admin_user, escalation_reason)
-
-
# 发送升级通知给高级管理员
-
send_escalation_notification(escalation_reason)
-
-
success!({
-
message: "审批已升级给高级管理员",
-
event: event_approval_info,
-
escalation_details: {
-
reason: escalation_reason,
-
escalated_by: admin_user_info,
-
escalated_at: Time.current
-
}
-
})
-
end
-
rescue => e
-
failure!("升级审批失败: #{e.message}")
-
end
-
-
# 辅助方法
-
-
# 验证活动是否满足审批条件
-
def validate_event_for_approval
-
errors = []
-
-
# 检查基本信息
-
errors << "活动标题不能为空" if event.title.blank?
-
errors << "活动描述不能为空" if event.description.blank?
-
errors << "书籍名称不能为空" if event.book_name.blank?
-
-
# 检查日期设置
-
errors << "开始日期不能为空" if event.start_date.blank?
-
errors << "结束日期不能为空" if event.end_date.blank?
-
errors << "开始日期必须在今天之后" if event.start_date <= Date.today
-
-
# 检查人数设置
-
errors << "最大参与人数必须大于0" if event.max_participants.nil? || event.max_participants <= 0
-
errors << "最小参与人数不能大于最大参与人数" if event.min_participants > event.max_participants
-
-
# 检查费用设置(如果是收费活动)
-
if event.fee_type != 'free'
-
errors << "收费活动必须设置费用金额" if event.fee_amount.nil? || event.fee_amount <= 0
-
errors << "收费活动必须设置领读人奖励比例" if event.leader_reward_percentage.nil?
-
end
-
-
# 检查阅读计划
-
if event.reading_schedules.empty?
-
errors << "必须设置阅读计划"
-
end
-
-
# 检查特定活动模式的特殊要求
-
case event.activity_mode
-
when 'video_conference'
-
errors << "视频会议活动必须设置会议链接" if event.meeting_link.blank?
-
when 'offline_meeting'
-
errors << "线下活动必须设置活动地点" if event.location.blank?
-
end
-
-
{
-
valid: errors.empty?,
-
errors: errors
-
}
-
end
-
-
# 应用审批队列过滤条件
-
def apply_approval_queue_filters(events, filters)
-
events = events.where(leader_id: filters[:leader_id]) if filters[:leader_id].present?
-
events = events.where(activity_mode: filters[:activity_mode]) if filters[:activity_mode].present?
-
events = events.where(fee_type: filters[:fee_type]) if filters[:fee_type].present?
-
events = events.where('submitted_for_approval_at >= ?', filters[:submitted_since]) if filters[:submitted_since].present?
-
events = events.where('submitted_for_approval_at <= ?', filters[:submitted_until]) if filters[:submitted_until].present?
-
-
events
-
end
-
-
# 计算平均审批时间
-
def calculate_average_approval_time(date_range)
-
approved_events = ReadingEvent.where(
-
approval_status: :approved,
-
approved_at: date_range
-
).where.not(submitted_for_approval_at: nil)
-
-
return 0 if approved_events.empty?
-
-
total_time = approved_events.sum do |event|
-
(event.approved_at - event.submitted_for_approval_at) / 1.hour
-
end
-
-
(total_time / approved_events.count).round(2)
-
end
-
-
# 计算审批通过率
-
def calculate_approval_rate(date_range)
-
total_events = ReadingEvent.where(
-
approved_at: date_range
-
).where.not(approval_status: :pending)
-
-
return 0 if total_events.empty?
-
-
approved_count = total_events.where(approval_status: :approved).count
-
(approved_count.to_f / total_events.count * 100).round(2)
-
end
-
-
# 获取管理员审批统计
-
def get_admin_approval_stats(date_range)
-
approved_events = ReadingEvent.includes(:approver)
-
.where(approval_status: :approved, approved_at: date_range)
-
.where.not(approved_by_id: nil)
-
-
stats = {}
-
approved_events.each do |event|
-
admin_id = event.approved_by_id
-
stats[admin_id] ||= {
-
name: event.approver&.nickname || 'Unknown',
-
approved_count: 0,
-
rejected_count: 0
-
}
-
stats[admin_id][:approved_count] += 1
-
end
-
-
rejected_events = ReadingEvent.includes(:approver)
-
.where(approval_status: :rejected, approved_at: date_range)
-
.where.not(approved_by_id: nil)
-
-
rejected_events.each do |event|
-
admin_id = event.approved_by_id
-
stats[admin_id] ||= {
-
name: event.approver&.nickname || 'Unknown',
-
approved_count: 0,
-
rejected_count: 0
-
}
-
stats[admin_id][:rejected_count] += 1
-
end
-
-
stats.values
-
end
-
-
# 获取活动模式审批统计
-
def get_activity_mode_approval_stats(date_range)
-
modes = %w[note_checkin free_discussion video_conference offline_meeting]
-
stats = {}
-
-
modes.each do |mode|
-
total = ReadingEvent.where(
-
activity_mode: mode,
-
approved_at: date_range
-
).where.not(approval_status: :pending).count
-
-
approved = ReadingEvent.where(
-
activity_mode: mode,
-
approval_status: :approved,
-
approved_at: date_range
-
).count
-
-
stats[mode] = {
-
total: total,
-
approved: approved,
-
rejected: total - approved,
-
approval_rate: total > 0 ? (approved.to_f / total * 100).round(2) : 0
-
}
-
end
-
-
stats
-
end
-
-
# 获取队列统计信息
-
def get_queue_statistics(events)
-
{
-
total_pending: events.count,
-
pending_by_fee_type: events.group(:fee_type).count,
-
pending_by_activity_mode: events.group(:activity_mode).count,
-
oldest_pending_age: events.maximum(:submitted_for_approval_at) ?
-
((Time.current - events.maximum(:submitted_for_approval_at)) / 1.day).round(1) : 0,
-
average_pending_age: events.average(:submitted_for_approval_at) ?
-
((Time.current - events.average(:submitted_for_approval_at)) / 1.day).round(1) : 0
-
}
-
end
-
-
# 创建审批日志
-
def create_approval_log(action, operator, reason, target_event = event)
-
# 这里应该创建一个 ApprovalLog 模型来记录审批历史
-
# 暂时使用 Rails logger 记录
-
Rails.logger.info "审批日志: #{action} - 活动 #{target_event.id} - 操作者 #{operator.nickname} - 理由: #{reason}"
-
end
-
-
# 发送审批决定通知
-
def send_approval_decision_notification(decision)
-
# 这里应该实现通知系统
-
Rails.logger.info "审批通知: 活动 #{event.id} 已被#{decision == :approved ? '通过' : '拒绝'}"
-
end
-
-
# 发送升级通知
-
def send_escalation_notification(reason)
-
# 这里应该实现升级通知给高级管理员
-
Rails.logger.info "审批升级: 活动 #{event.id} 需要高级管理员审批 - 理由: #{reason}"
-
end
-
-
# 获取审批队列位置
-
def get_approval_queue_position
-
ReadingEvent.where(approval_status: :pending)
-
.where('submitted_for_approval_at <= ?', event.submitted_for_approval_at)
-
.count
-
end
-
-
# 格式化活动审批信息
-
def event_approval_info
-
{
-
id: event.id,
-
title: event.title,
-
book_name: event.book_name,
-
activity_mode: event.activity_mode,
-
fee_type: event.fee_type,
-
fee_amount: event.fee_amount,
-
max_participants: event.max_participants,
-
start_date: event.start_date,
-
end_date: event.end_date,
-
leader: user_info(event.leader),
-
status: event.status,
-
approval_status: event.approval_status,
-
submitted_for_approval_at: event.submitted_for_approval_at,
-
approved_at: event.approved_at,
-
approver: event.approver ? user_info(event.approver) : nil
-
}
-
end
-
-
# 格式化活动审批队列信息
-
def event_approval_queue_info(event_item)
-
{
-
id: event_item.id,
-
title: event_item.title,
-
book_name: event_item.book_name,
-
activity_mode: event_item.activity_mode,
-
fee_type: event_item.fee_type,
-
fee_amount: event_item.fee_amount,
-
max_participants: event_item.max_participants,
-
start_date: event_item.start_date,
-
end_date: event_item.end_date,
-
leader: user_info(event_item.leader),
-
submitted_for_approval_at: event_item.submitted_for_approval_at,
-
pending_age_days: event_item.submitted_for_approval_at ?
-
((Time.current - event_item.submitted_for_approval_at) / 1.day).round(1) : 0,
-
validation_status: event_item.validate_event_for_approval[:valid] ? 'valid' : 'invalid',
-
requires_attention: requires_immediate_attention?(event_item)
-
}
-
end
-
-
# 格式化审批决定信息
-
def approval_decision_info
-
{
-
approved_by: user_info(admin_user),
-
approved_at: Time.current,
-
reason: @approval_options[:reason],
-
notes: @approval_options[:notes],
-
next_steps: get_next_steps_after_approval
-
}
-
end
-
-
# 获取审批后的下一步操作
-
def get_next_steps_after_approval
-
if @action == :approve
-
[
-
"活动已进入报名状态",
-
"系统已自动通知活动创建者",
-
"参与者现在可以报名参加活动",
-
"活动将在开始日期自动开始"
-
]
-
else
-
[
-
"活动已被拒绝",
-
"创建者可以根据拒绝理由修改活动",
-
"修改后可以重新提交审批"
-
]
-
end
-
end
-
-
# 检查活动是否需要立即关注
-
def requires_immediate_attention?(event_item)
-
# 检查是否即将开始
-
return true if event_item.start_date && event_item.start_date <= Date.today + 3.days
-
-
# 检查是否已经提交很久
-
return true if event_item.submitted_for_approval_at &&
-
(Time.current - event_item.submitted_for_approval_at) > 7.days
-
-
false
-
end
-
-
# 格式化用户信息
-
def user_info(user)
-
return nil unless user
-
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
}
-
end
-
-
def admin_user_info
-
user_info(admin_user)
-
end
-
end
-
# 分析服务
-
# 负责提供系统各方面的统计分析功能
-
class AnalyticsService
-
class << self
-
# 获取系统总览统计
-
def system_overview
-
{
-
users: user_statistics,
-
events: event_statistics,
-
check_ins: check_in_statistics,
-
flowers: flower_statistics,
-
notifications: notification_statistics,
-
engagement: engagement_statistics
-
}
-
end
-
-
# 用户统计
-
def user_statistics
-
{
-
total_users: User.count,
-
active_users_7_days: active_users_count(7.days.ago),
-
active_users_30_days: active_users_count(30.days.ago),
-
new_users_today: User.where('created_at >= ?', Date.current).count,
-
new_users_7_days: User.where('created_at >= ?', 7.days.ago).count,
-
new_users_30_days: User.where('created_at >= ?', 30.days.ago).count,
-
user_roles: User.group(:role).count
-
}
-
end
-
-
# 活动统计
-
def event_statistics
-
{
-
total_events: ReadingEvent.count,
-
active_events: ReadingEvent.where(status: ['enrolling', 'in_progress']).count,
-
completed_events: ReadingEvent.where(status: 'completed').count,
-
draft_events: ReadingEvent.where(status: 'draft').count,
-
events_7_days: ReadingEvent.where('created_at >= ?', 7.days.ago).count,
-
events_30_days: ReadingEvent.where('created_at >= ?', 30.days.ago).count,
-
approval_stats: {
-
pending: ReadingEvent.where(approval_status: 'pending').count,
-
approved: ReadingEvent.where(approval_status: 'approved').count,
-
rejected: ReadingEvent.where(approval_status: 'rejected').count
-
},
-
activity_modes: ReadingEvent.group(:activity_mode).count
-
}
-
end
-
-
# 打卡统计
-
def check_in_statistics
-
all_check_ins = CheckIn.all
-
-
# 计算质量分布(使用计算属性)
-
quality_scores = all_check_ins.map(&:quality_score).compact
-
quality_distribution = quality_scores.group_by { |score| score / 10 * 10 }.transform_values(&:count)
-
-
{
-
total_check_ins: all_check_ins.count,
-
check_ins_today: all_check_ins.where('created_at >= ?', Date.current).count,
-
check_ins_7_days: all_check_ins.where('created_at >= ?', 7.days.ago).count,
-
check_ins_30_days: all_check_ins.where('created_at >= ?', 30.days.ago).count,
-
quality_distribution: quality_distribution,
-
status_distribution: all_check_ins.group(:status).count,
-
average_word_count: all_check_ins.average(:word_count)&.round(2) || 0,
-
high_quality_rate: ((all_check_ins.select(&:high_quality?).count.to_f / all_check_ins.count * 100).round(2) rescue 0)
-
}
-
end
-
-
# 小红花统计
-
def flower_statistics
-
{
-
total_flowers: Flower.count,
-
flowers_today: Flower.where('created_at >= ?', Date.current).count,
-
flowers_7_days: Flower.where('created_at >= ?', 7.days.ago).count,
-
flowers_30_days: Flower.where('created_at >= ?', 30.days.ago).count,
-
unique_givers: Flower.distinct.count(:giver_id),
-
unique_receivers: Flower.distinct.count(:recipient_id),
-
flower_types: Flower.group(:flower_type).count,
-
average_amount: Flower.average(:amount)&.round(2) || 0,
-
comments_count: Flower.where.not(comment: [nil, '']).count
-
}
-
end
-
-
# 通知统计
-
def notification_statistics
-
{
-
total_notifications: Notification.count,
-
notifications_today: Notification.where('created_at >= ?', Date.current).count,
-
notifications_7_days: Notification.where('created_at >= ?', 7.days.ago).count,
-
notifications_30_days: Notification.where('created_at >= ?', 30.days.ago).count,
-
unread_notifications: Notification.where(read: false).count,
-
read_rate: ((Notification.where(read: true).count.to_f / Notification.count * 100).round(2) rescue 0),
-
notification_types: Notification.group(:notification_type).count
-
}
-
end
-
-
# 参与度统计
-
def engagement_statistics
-
{
-
average_event_participants: average_participants_per_event,
-
average_check_ins_per_event: average_check_ins_per_event,
-
average_flowers_per_event: average_flowers_per_event,
-
user_retention_rate: user_retention_rate,
-
daily_active_users: daily_active_users_data(7.days.ago),
-
popular_event_types: most_popular_event_types
-
}
-
end
-
-
# 用户详细统计
-
def user_analytics(user, days = 30)
-
start_date = days.days.ago
-
-
{
-
profile: user_profile_stats(user, start_date),
-
participation: user_participation_stats(user, start_date),
-
engagement: user_engagement_stats(user, start_date),
-
achievements: user_achievement_stats(user, start_date)
-
}
-
end
-
-
# 活动详细统计
-
def event_analytics(event)
-
{
-
overview: event_overview_stats(event),
-
participation: event_participation_stats(event),
-
engagement: event_engagement_stats(event),
-
timeline: event_timeline_stats(event),
-
feedback: event_feedback_stats(event)
-
}
-
end
-
-
# 趋势数据
-
def trend_data(metric, period = :week, days = 30)
-
start_date = days.days.ago
-
data_points = generate_time_points(start_date, period)
-
-
data_points.map do |date|
-
value = case metric
-
when :check_ins
-
CheckIn.where(created_at: date..end_of_period(date, period)).count
-
when :flowers
-
Flower.where(created_at: date..end_of_period(date, period)).count
-
when :users
-
User.where(created_at: date..end_of_period(date, period)).count
-
when :events
-
ReadingEvent.where(created_at: date..end_of_period(date, period)).count
-
when :notifications
-
Notification.where(created_at: date..end_of_period(date, period)).count
-
else
-
0
-
end
-
-
{
-
date: date.strftime('%Y-%m-%d'),
-
value: value
-
}
-
end
-
end
-
-
# 排行榜数据
-
def leaderboards(type = :flowers, limit = 10, period = :all_time)
-
case type
-
when :flowers
-
flowers_leaderboard(limit, period)
-
when :check_ins
-
check_ins_leaderboard(limit, period)
-
when :participation
-
participation_leaderboard(limit, period)
-
else
-
[]
-
end
-
end
-
-
private
-
-
# 活跃用户数量
-
def active_users_count(since)
-
User.joins(:check_ins)
-
.where('check_ins.created_at >= ?', since)
-
.distinct
-
.count
-
end
-
-
# 每个活动的平均参与人数
-
def average_participants_per_event
-
return 0 if ReadingEvent.count == 0
-
-
total_participants = ReadingEvent.joins(:event_enrollments)
-
.where(event_enrollments: { status: 'enrolled' })
-
.count
-
(total_participants.to_f / ReadingEvent.count).round(2)
-
end
-
-
# 每个活动的平均打卡数
-
def average_check_ins_per_event
-
return 0 if ReadingEvent.count == 0
-
-
total_check_ins = CheckIn.joins(reading_event: :event_enrollments)
-
.count
-
(total_check_ins.to_f / ReadingEvent.count).round(2)
-
end
-
-
# 每个活动的平均小红花数
-
def average_flowers_per_event
-
return 0 if ReadingEvent.count == 0
-
-
total_flowers = Flower.joins(check_in: { reading_event: :event_enrollments })
-
.count
-
(total_flowers.to_f / ReadingEvent.count).round(2)
-
end
-
-
# 用户留存率
-
def user_retention_rate
-
new_users_30_days_ago = User.where('created_at BETWEEN ? AND ?', 60.days.ago, 30.days.ago)
-
return 0 if new_users_30_days_ago.count == 0
-
-
retained_users = new_users_30_days_ago.joins(:check_ins)
-
.where('check_ins.created_at >= ?', 30.days.ago)
-
.distinct
-
.count
-
-
(retained_users.to_f / new_users_30_days_ago.count * 100).round(2)
-
end
-
-
# 每日活跃用户数据
-
def daily_active_users_data(since)
-
(since.to_date..Date.current).map do |date|
-
active_users = User.joins(:check_ins)
-
.where('check_ins.created_at >= ? AND check_ins.created_at < ?',
-
date.beginning_of_day, date.end_of_day)
-
.distinct
-
.count
-
-
{
-
date: date.strftime('%Y-%m-%d'),
-
active_users: active_users
-
}
-
end
-
end
-
-
# 最受欢迎的活动类型
-
def most_popular_event_types
-
ReadingEvent.joins(:event_enrollments)
-
.group('reading_events.activity_mode')
-
.count('event_enrollments.id')
-
.sort_by { |_, count| -count }
-
.first(5)
-
.map { |mode, count| { activity_mode: mode, participants: count } }
-
end
-
-
# 用户档案统计
-
def user_profile_stats(user, start_date)
-
{
-
user_id: user.id,
-
nickname: user.nickname,
-
member_since: user.created_at,
-
last_active: last_activity_date(user),
-
participation_score: calculate_participation_score(user, start_date),
-
influence_score: calculate_influence_score(user, start_date)
-
}
-
end
-
-
# 用户参与统计
-
def user_participation_stats(user, start_date)
-
enrollments = user.event_enrollments.where('created_at >= ?', start_date)
-
-
{
-
events_enrolled: enrollments.count,
-
events_completed: enrollments.where(status: 'completed').count,
-
completion_rate: calculate_completion_rate(enrollments),
-
check_ins_total: user.check_ins.where('created_at >= ?', start_date).count,
-
average_check_ins_per_event: calculate_avg_check_ins_per_event(enrollments)
-
}
-
end
-
-
# 用户互动统计
-
def user_engagement_stats(user, start_date)
-
{
-
flowers_given: user.given_flowers.where('created_at >= ?', start_date).count,
-
flowers_received: user.received_flowers.where('created_at >= ?', start_date).count,
-
comments_given: user.comments.where('created_at >= ?', start_date).count,
-
notifications_sent: Notification.where(actor: user).where('created_at >= ?', start_date).count,
-
interaction_score: calculate_interaction_score(user, start_date)
-
}
-
end
-
-
# 用户成就统计
-
def user_achievement_stats(user, start_date)
-
{
-
certificates_count: user.flower_certificates.where('created_at >= ?', start_date).count,
-
top_three_finishes: user.flower_certificates.where(rank: [1, 2, 3]).count,
-
high_quality_check_ins: user.check_ins.where('created_at >= ?', start_date).select(&:high_quality?).count,
-
streak_days: calculate_current_streak(user),
-
achievements: get_user_achievements(user, start_date)
-
}
-
end
-
-
# 活动概览统计
-
def event_overview_stats(event)
-
{
-
event_id: event.id,
-
title: event.title,
-
status: event.status,
-
approval_status: event.approval_status,
-
created_at: event.created_at,
-
start_date: event.start_date,
-
end_date: event.end_date,
-
duration_days: event.days_count,
-
activity_mode: event.activity_mode
-
}
-
end
-
-
# 活动参与统计
-
def event_participation_stats(event)
-
enrollments = event.event_enrollments
-
-
{
-
total_enrollments: enrollments.count,
-
active_enrollments: enrollments.where(status: 'enrolled').count,
-
completed_enrollments: enrollments.where(status: 'completed').count,
-
completion_rate: calculate_event_completion_rate(enrollments),
-
average_completion_rate: enrollments.average(:completion_rate)&.round(2) || 0,
-
participation_trend: participation_trend_data(event)
-
}
-
end
-
-
# 活动互动统计
-
def event_engagement_stats(event)
-
{
-
total_check_ins: event.check_ins.count,
-
unique_participants_checking_in: event.check_ins.distinct.count(:user_id),
-
average_check_ins_per_participant: calculate_avg_check_ins(event),
-
total_flowers: event.flowers_count,
-
flowers_per_check_in: calculate_flowers_per_check_in(event),
-
engagement_score: calculate_event_engagement_score(event)
-
}
-
end
-
-
# 活动时间线统计
-
def event_timeline_stats(event)
-
if event.status == 'completed'
-
{
-
total_duration: event.days_count,
-
actual_start_date: event.reading_schedules.minimum(:date),
-
actual_end_date: event.reading_schedules.maximum(:date),
-
peak_activity_day: find_peak_activity_day(event),
-
daily_participation: daily_participation_data(event)
-
}
-
else
-
{
-
planned_duration: event.days_count,
-
progress_percentage: calculate_event_progress(event),
-
current_day: calculate_current_event_day(event),
-
daily_activity: daily_activity_data(event)
-
}
-
end
-
end
-
-
# 活动反馈统计
-
def event_feedback_stats(event)
-
{
-
average_rating: calculate_average_rating(event),
-
feedback_count: count_feedback_responses(event),
-
satisfaction_rate: calculate_satisfaction_rate(event),
-
common_themes: analyze_feedback_themes(event)
-
}
-
end
-
-
# 生成时间点
-
def generate_time_points(start_date, period)
-
case period
-
when :day
-
(start_date.to_date..Date.current).to_a
-
when :week
-
weeks = []
-
current = start_date.to_date.beginning_of_week
-
while current <= Date.current
-
weeks << current
-
current += 1.week
-
end
-
weeks
-
when :month
-
months = []
-
current = start_date.to_date.beginning_of_month
-
while current <= Date.current
-
months << current
-
current += 1.month
-
end
-
months
-
else
-
[start_date.to_date]
-
end
-
end
-
-
# 期间的结束时间
-
def end_of_period(date, period)
-
case period
-
when :day
-
date.end_of_day
-
when :week
-
date.end_of_week
-
when :month
-
date.end_of_month
-
else
-
date.end_of_day
-
end
-
end
-
-
# 小红花排行榜
-
def flowers_leaderboard(limit, period)
-
flowers_query = Flower.all
-
-
case period
-
when :today
-
flowers_query = flowers_query.where('created_at >= ?', Date.current)
-
when :week
-
flowers_query = flowers_query.where('created_at >= ?', 1.week.ago)
-
when :month
-
flowers_query = flowers_query.where('created_at >= ?', 1.month.ago)
-
end
-
-
# 简化实现:先查询小红花,然后按用户分组统计
-
user_flower_stats = {}
-
-
flowers_query.includes(:recipient).find_each do |flower|
-
recipient_id = flower.recipient_id
-
user_flower_stats[recipient_id] ||= {
-
user: flower.recipient,
-
total_flowers: 0,
-
flowers_count: 0
-
}
-
-
user_flower_stats[recipient_id][:total_flowers] += flower.amount || 1
-
user_flower_stats[recipient_id][:flowers_count] += 1
-
end
-
-
# 按总数排序并限制数量
-
user_flower_stats.values
-
.sort_by { |stats| -stats[:total_flowers] }
-
.first(limit)
-
.map.with_index(1) do |stats, index|
-
{
-
user: stats[:user].as_json_for_api,
-
total_flowers: stats[:total_flowers],
-
flowers_count: stats[:flowers_count],
-
rank: index
-
}
-
end
-
end
-
-
# 打卡排行榜
-
def check_ins_leaderboard(limit, period)
-
check_ins_query = CheckIn.all
-
-
case period
-
when :today
-
check_ins_query = check_ins_query.where('created_at >= ?', Date.current)
-
when :week
-
check_ins_query = check_ins_query.where('created_at >= ?', 1.week.ago)
-
when :month
-
check_ins_query = check_ins_query.where('created_at >= ?', 1.month.ago)
-
end
-
-
# 简化实现:先查询打卡,然后按用户分组统计
-
user_check_in_stats = {}
-
-
check_ins_query.includes(:user).find_each do |check_in|
-
user_id = check_in.user_id
-
user_check_in_stats[user_id] ||= {
-
user: check_in.user,
-
check_ins_count: 0,
-
quality_scores: []
-
}
-
-
user_check_in_stats[user_id][:check_ins_count] += 1
-
user_check_in_stats[user_id][:quality_scores] << check_in.quality_score if check_in.quality_score
-
end
-
-
# 计算平均质量分并排序
-
user_check_in_stats.values
-
.map do |stats|
-
avg_quality = if stats[:quality_scores].any?
-
stats[:quality_scores].sum.to_f / stats[:quality_scores].count
-
else
-
0
-
end
-
-
{
-
user: stats[:user].as_json_for_api,
-
check_ins_count: stats[:check_ins_count],
-
average_quality: avg_quality.round(2),
-
rank: 0
-
}
-
end
-
.sort_by { |stats| [-stats[:check_ins_count], -stats[:average_quality]] }
-
.first(limit)
-
.map.with_index(1) { |stats, index| stats.merge(rank: index) }
-
end
-
-
# 参与度排行榜
-
def participation_leaderboard(limit, period)
-
enrollments_query = EventEnrollment.all
-
-
case period
-
when :today
-
enrollments_query = enrollments_query.where('created_at >= ?', Date.current)
-
when :week
-
enrollments_query = enrollments_query.where('created_at >= ?', 1.week.ago)
-
when :month
-
enrollments_query = enrollments_query.where('created_at >= ?', 1.month.ago)
-
end
-
-
# 简化实现:先查询报名,然后按用户分组统计
-
user_enrollment_stats = {}
-
-
enrollments_query.includes(:user).find_each do |enrollment|
-
user_id = enrollment.user_id
-
event_id = enrollment.reading_event_id
-
completion_rate = enrollment.completion_rate || 0
-
-
user_enrollment_stats[user_id] ||= {
-
user: enrollment.user,
-
events_count: 0,
-
event_ids: Set.new,
-
completion_rates: []
-
}
-
-
user_enrollment_stats[user_id][:events_count] += 1
-
user_enrollment_stats[user_id][:event_ids] << event_id
-
user_enrollment_stats[user_id][:completion_rates] << completion_rate
-
end
-
-
# 计算统计数据并排序
-
user_enrollment_stats.values
-
.map do |stats|
-
unique_events = stats[:event_ids].size
-
avg_completion = if stats[:completion_rates].any?
-
stats[:completion_rates].sum.to_f / stats[:completion_rates].count
-
else
-
0
-
end
-
-
{
-
user: stats[:user].as_json_for_api,
-
events_count: unique_events,
-
average_completion: avg_completion.round(2),
-
rank: 0
-
}
-
end
-
.sort_by { |stats| [-stats[:events_count], -stats[:average_completion]] }
-
.first(limit)
-
.map.with_index(1) { |stats, index| stats.merge(rank: index) }
-
end
-
-
# 辅助方法(计算各种指标)
-
def last_activity_date(user)
-
user.check_ins.maximum(:created_at) ||
-
user.flowers.maximum(:created_at) ||
-
user.comments.maximum(:created_at) ||
-
user.created_at
-
end
-
-
def calculate_participation_score(user, start_date)
-
# 基于活动参与、打卡次数、完成率等计算
-
enrollments = user.event_enrollments.where('created_at >= ?', start_date)
-
check_ins = user.check_ins.where('created_at >= ?', start_date)
-
-
base_score = enrollments.count * 10
-
check_in_score = check_ins.count * 5
-
completion_bonus = enrollments.where(status: 'completed').count * 20
-
-
base_score + check_in_score + completion_bonus
-
end
-
-
def calculate_influence_score(user, start_date)
-
# 基于小红花、评论、通知等计算影响力
-
flowers_given = user.given_flowers.where('created_at >= ?', start_date).count
-
comments = user.comments.where('created_at >= ?', start_date).count
-
-
flowers_given * 15 + comments * 10
-
end
-
-
def calculate_completion_rate(enrollments)
-
return 0 if enrollments.empty?
-
completed = enrollments.where(status: 'completed').count
-
(completed.to_f / enrollments.count * 100).round(2)
-
end
-
-
def calculate_avg_check_ins_per_event(enrollments)
-
return 0 if enrollments.empty?
-
# 避免JOIN查询中的列名冲突,改用子查询
-
check_in_ids = CheckIn.where(enrollment_id: enrollments.pluck(:id)).pluck(:id)
-
total_check_ins = check_in_ids.count
-
(total_check_ins.to_f / enrollments.count).round(2)
-
end
-
-
def calculate_interaction_score(user, start_date)
-
flowers_received = user.received_flowers.where('created_at >= ?', start_date).count
-
# 这里简化处理,因为 Comment 可能不直接与 User 关联
-
comments_received = 0 # Comment.where(commentable: user).where('created_at >= ?', start_date).count
-
-
flowers_received * 10 + comments_received * 5
-
end
-
-
def calculate_current_streak(user)
-
# 计算用户当前的打卡连续天数
-
# 这里简化实现,实际可以更复杂
-
recent_check_ins = user.check_ins.where('created_at >= ?', 30.days.ago)
-
.order(created_at: :desc)
-
-
return 0 if recent_check_ins.empty?
-
-
streak = 0
-
current_date = Date.current
-
-
recent_check_ins.each do |check_in|
-
if check_in.created_at.to_date == current_date
-
streak += 1
-
current_date -= 1.day
-
else
-
break
-
end
-
end
-
-
streak
-
end
-
-
def get_user_achievements(user, start_date)
-
# 获取用户成就徽章等
-
achievements = []
-
-
# 基于各种条件授予成就
-
if user.check_ins.where('created_at >= ?', start_date).count >= 30
-
achievements << { name: '勤奋打卡', description: '30天内打卡超过30次' }
-
end
-
-
if user.received_flowers.where('created_at >= ?', start_date).count >= 10
-
achievements << { name: '人气之星', description: '30天内收到超过10朵小红花' }
-
end
-
-
achievements
-
end
-
-
def calculate_event_completion_rate(enrollments)
-
return 0 if enrollments.empty?
-
completed = enrollments.where(status: 'completed').count
-
(completed.to_f / enrollments.count * 100).round(2)
-
end
-
-
def calculate_avg_check_ins(event)
-
return 0 if event.event_enrollments.empty?
-
total_check_ins = event.check_ins.count
-
(total_check_ins.to_f / event.event_enrollments.count).round(2)
-
end
-
-
def calculate_flowers_per_check_in(event)
-
check_ins_count = event.check_ins.count
-
flowers_count = event.flowers_count
-
-
return 0 if check_ins_count == 0
-
(flowers_count.to_f / check_ins_count).round(2)
-
end
-
-
def calculate_event_engagement_score(event)
-
# 综合评分:打卡率 + 小红花率 + 完成率
-
check_in_rate = [calculate_avg_check_ins(event) * 10, 100].min
-
flower_rate = [calculate_flowers_per_check_in(event) * 20, 100].min
-
completion_rate = calculate_event_completion_rate(event.event_enrollments)
-
-
(check_in_rate * 0.4 + flower_rate * 0.3 + completion_rate * 0.3).round(2)
-
end
-
-
def calculate_event_progress(event)
-
return 100 if event.status == 'completed'
-
return 0 if event.status == 'draft'
-
-
total_days = event.days_count
-
return 0 if total_days == 0
-
-
elapsed_days = [(Date.current - event.start_date).to_i, 0].max
-
[elapsed_days.to_f / total_days * 100, 100].min.round(2)
-
end
-
-
def calculate_current_event_day(event)
-
return 0 if event.start_date.nil?
-
-
elapsed = [(Date.current - event.start_date).to_i + 1, 1].max
-
[elapsed, event.days_count].min
-
end
-
-
# 更多辅助方法可以根据需要继续添加...
-
end
-
end
-
# frozen_string_literal: true
-
-
# API性能优化服务
-
# 提供API请求限流、分页优化、批量操作等功能
-
class ApiPerformanceService
-
class << self
-
# API请求限流
-
# @param identifier [String] 请求标识符(用户ID、IP地址等)
-
# @param limit [Integer] 请求限制数量
-
# @param period [Integer] 时间周期(秒)
-
# @param cache_key_prefix [String] 缓存键前缀
-
# @return [Hash] 限流结果
-
def rate_limit(identifier, limit: 100, period: 60, cache_key_prefix: 'rate_limit')
-
cache_key = "#{cache_key_prefix}:#{identifier}:#{Time.current.to_i / period}"
-
current_count = Rails.cache.read(cache_key) || 0
-
-
if current_count >= limit
-
{
-
allowed: false,
-
remaining: 0,
-
reset_time: (Time.current.to_i / period + 1) * period,
-
retry_after: period - (Time.current.to_i % period)
-
}
-
else
-
# 增加计数
-
Rails.cache.write(cache_key, current_count + 1, expires_in: period)
-
-
{
-
allowed: true,
-
remaining: limit - current_count - 1,
-
reset_time: (Time.current.to_i / period + 1) * period,
-
current_count: current_count + 1
-
}
-
end
-
end
-
-
# 智能API响应格式化
-
# @param success [Boolean] 请求是否成功
-
# @param data [Object] 响应数据
-
# @param message [String] 响应消息
-
# @param meta [Hash] 元数据
-
# @param status_code [Integer] HTTP状态码
-
# @return [Hash] 格式化的API响应
-
def format_api_response(success: true, data: nil, message: nil, meta: {}, status_code: 200)
-
response = {
-
success: success,
-
message: message,
-
data: data,
-
meta: meta
-
}
-
-
# 添加时间戳
-
response[:timestamp] = Time.current.iso8601
-
-
# 添加请求ID(如果存在)
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
response
-
end
-
-
# 优化的分页响应
-
# @param pagination_result [Hash] 分页结果
-
# @param additional_meta [Hash] 额外的元数据
-
# @return [Hash] 格式化的分页响应
-
def format_paginated_response(pagination_result, additional_meta = {})
-
meta = pagination_result[:pagination] || {}
-
meta.merge!(additional_meta)
-
-
format_api_response(
-
success: true,
-
data: pagination_result[:records],
-
meta: meta
-
)
-
end
-
-
# 批量操作支持
-
# @param records [Array] 记录数组
-
# @param batch_size [Integer] 批次大小
-
# @param options [Hash] 操作选项
-
# @yield [Array] 每批记录
-
# @return [Array] 所有操作结果
-
def batch_process(records, batch_size: 50, options = {})
-
return [] if records.empty?
-
-
results = []
-
total_batches = (records.length.to_f / batch_size).ceil
-
current_batch = 1
-
-
records.each_slice(batch_size) do |batch|
-
begin
-
batch_result = yield(batch) if block_given?
-
-
if batch_result.is_a?(Array)
-
results.concat(batch_result)
-
else
-
results << batch_result
-
end
-
-
# 批次操作日志
-
Rails.logger.info "批量操作进度: #{current_batch}/#{total_batches} 批次完成" if options[:log_progress]
-
-
rescue => e
-
Rails.logger.error "批量操作失败 (批次 #{current_batch}): #{e.message}"
-
-
if options[:continue_on_error]
-
results << { error: e.message, batch: current_batch }
-
else
-
raise e
-
end
-
end
-
-
current_batch += 1
-
end
-
-
results
-
end
-
-
# API字段选择器
-
# @param records [Array] 记录数组
-
# @param fields [Array] 需要返回的字段
-
# @param options [Hash] 选项
-
# @return [Array] 选择字段后的记录
-
def select_fields(records, fields, options = {})
-
return records if fields.blank? || records.empty?
-
-
records.map do |record|
-
if record.respond_to?(:as_json_for_api)
-
# 如果记录支持as_json_for_api方法
-
record.as_json_for_api(options.slice(:includes))
-
else
-
record.as_json
-
end.slice(*fields.map(&:to_s))
-
end
-
end
-
-
# 数据压缩(对大型响应进行压缩)
-
# @param data [Hash] 要压缩的数据
-
# @param threshold [Integer] 压缩阈值(字符数)
-
# @return [Hash] 压缩后的数据
-
def compress_if_needed(data, threshold: 10240) # 10KB
-
return data unless should_compress?(data, threshold)
-
-
compressed_data = {
-
compressed: true,
-
data: compress_data(data),
-
original_size: data.to_s.length,
-
compressed_size: compress_data(data).length
-
}
-
end
-
-
# API缓存装饰器
-
# @param cache_key [String] 缓存键
-
# @param ttl [Integer] 缓存时间(秒)
-
# @param options [Hash] 缓存选项
-
# @yield 要缓存的操作
-
# @return [Object] 缓存的结果
-
def cache_api_response(cache_key, ttl: 5.minutes, options = {})
-
# 如果用户未登录,不缓存
-
return yield unless options[:skip_auth_check] || RequestStore.store[:current_user]
-
-
full_cache_key = "api_response:#{cache_key}:#{RequestStore.store[:current_user]&.id}:#{RequestStore.store[:user_role]}"
-
-
Rails.cache.fetch(full_cache_key, expires_in: ttl, race_condition_ttl: 30.seconds) do
-
yield
-
end
-
end
-
-
# API性能监控
-
# @param endpoint [String] API端点
-
# @param method [String] HTTP方法
-
# @param options [Hash] 监控选项
-
# @yield 要监控的操作
-
# @return [Object] 操作结果
-
def monitor_performance(endpoint, method: 'GET', options = {})
-
start_time = Time.current
-
-
begin
-
result = yield
-
-
execution_time = Time.current - start_time
-
log_performance_metrics(endpoint, method, execution_time, options, true)
-
-
result
-
rescue => e
-
execution_time = Time.current - start_time
-
log_performance_metrics(endpoint, method, execution_time, options, false, e)
-
-
raise e
-
end
-
end
-
-
# 请求参数验证和清理
-
# @param params [Hash] 请求参数
-
# @param allowed_params [Array] 允许的参数列表
-
# @param options [Hash] 验证选项
-
# @return [Hash] 清理后的参数
-
def sanitize_params(params, allowed_params, options = {})
-
return {} if params.blank?
-
-
# 只保留允许的参数
-
sanitized = params.slice(*allowed_params)
-
-
# 类型转换
-
sanitized = convert_param_types(sanitized, options[:type_conversions] || {})
-
-
# 验证必填参数
-
if options[:required]&.any?
-
missing_params = options[:required] - sanitized.keys
-
if missing_params.any?
-
raise ArgumentError, "缺少必填参数: #{missing_params.join(', ')}"
-
end
-
end
-
-
# 参数值验证
-
if options[:validations]
-
validate_param_values(sanitized, options[:validations])
-
end
-
-
sanitized
-
end
-
-
# 响应时间优化:异步处理非关键操作
-
# @param operation [Symbol] 操作类型
-
# @param data [Object] 操作数据
-
# @param options [Hash] 操作选项
-
def async_process(operation, data, options = {})
-
# 使用Rails的Active Job进行异步处理
-
case operation
-
when :send_notification
-
NotificationJob.perform_later(data, options)
-
when :update_statistics
-
StatisticsJob.perform_later(data, options)
-
when :send_email
-
EmailJob.perform_later(data, options)
-
when :generate_report
-
ReportJob.perform_later(data, options)
-
else
-
Rails.logger.warn "未知的异步操作类型: #{operation}"
-
end
-
end
-
-
# 响应压缩中间件支持
-
# @param response_body [String] 响应体
-
# @param request_headers [Hash] 请求头
-
# @return [String] 压缩后的响应体
-
def compress_response(response_body, request_headers = {})
-
# 检查客户端是否支持压缩
-
accept_encoding = request_headers['Accept-Encoding'] || ''
-
return response_body unless accept_encoding.include?('gzip')
-
-
# 压缩响应
-
require 'zlib'
-
compressed = Zlib::Deflate.deflate(response_body)
-
-
# 添加压缩标记
-
response_body
-
end
-
-
# API版本控制支持
-
# @param request [ActionDispatch::Request] 请求对象
-
# @param available_versions [Array] 可用的API版本
-
# @return [String] 选择的API版本
-
def determine_api_version(request, available_versions = ['v1'])
-
# 从URL路径获取版本
-
version_from_path = request.path.split('/')[1]
-
return version_from_path if available_versions.include?(version_from_path)
-
-
# 从请求头获取版本
-
version_from_header = request.headers['API-Version']
-
return version_from_header if available_versions.include?(version_from_header)
-
-
# 返回默认版本
-
available_versions.first
-
end
-
-
# 响应格式协商
-
# @param request [ActionDispatch::Request] 请求对象
-
# @param data [Object] 响应数据
-
# @param default_format [Symbol] 默认格式
-
# @return [String] 格式化后的响应
-
def negotiate_response_format(request, data, default_format = :json)
-
accept_header = request.headers['Accept'] || 'application/json'
-
-
case accept_header
-
when /json/
-
data.to_json
-
when /xml/
-
data.respond_to?(:to_xml) ? data.to_xml : data.to_json
-
when /text/
-
data.to_s
-
else
-
case default_format
-
when :json
-
data.to_json
-
when :xml
-
data.respond_to?(:to_xml) ? data.to_xml : data.to_json
-
else
-
data.to_s
-
end
-
end
-
end
-
-
private
-
-
# 判断是否需要压缩
-
def should_compress?(data, threshold)
-
data.to_s.length > threshold
-
end
-
-
# 压缩数据
-
def compress_data(data)
-
require 'zlib'
-
Base64.strict_encode64(Zlib::Deflate.deflate(data.to_json))
-
end
-
-
# 记录性能指标
-
def log_performance_metrics(endpoint, method, execution_time, options, success, error = nil)
-
metrics = {
-
endpoint: endpoint,
-
method: method,
-
execution_time: execution_time.round(3),
-
success: success,
-
timestamp: Time.current.iso8601,
-
user_id: RequestStore.store[:current_user]&.id,
-
user_role: RequestStore.store[:user_role]
-
}
-
-
if error
-
metrics[:error] = {
-
message: error.message,
-
class: error.class.name
-
}
-
end
-
-
# 记录到日志
-
if success && execution_time > 1.0
-
Rails.logger.warn "慢查询警告: #{metrics}"
-
elsif !success
-
Rails.logger.error "API错误: #{metrics}"
-
end
-
-
# 发送到监控系统(如果配置了)
-
if defined?(StatsD)
-
StatsD.timing("api.#{endpoint.gsub('/', '_')}.#{method.downcase}", execution_time * 1000)
-
StatsD.increment("api.#{endpoint.gsub('/', '_')}.#{method.downcase}.#{success ? 'success' : 'error'}")
-
end
-
end
-
-
# 参数类型转换
-
def convert_param_types(params, type_conversions)
-
converted = params.dup
-
-
type_conversions.each do |key, type|
-
next unless converted.key?(key)
-
-
case type
-
when :integer
-
converted[key] = converted[key].to_i
-
when :float
-
converted[key] = converted[key].to_f
-
when :boolean
-
converted[key] = %w[true yes 1 t].include?(converted[key].to_s.downcase)
-
when :date
-
converted[key] = Date.parse(converted[key]) rescue nil
-
when :datetime
-
converted[key] = DateTime.parse(converted[key]) rescue nil
-
end
-
end
-
-
converted
-
end
-
-
# 参数值验证
-
def validate_param_values(params, validations)
-
validations.each do |key, rules|
-
next unless params.key?(key)
-
-
value = params[key]
-
-
# 必填验证
-
if rules[:required] && value.blank?
-
raise ArgumentError, "参数 #{key} 是必填的"
-
end
-
-
# 范围验证
-
if rules[:range] && value.present?
-
min_val, max_val = rules[:range]
-
if min_val && value < min_val
-
raise ArgumentError, "参数 #{key} 不能小于 #{min_val}"
-
end
-
if max_val && value > max_val
-
raise ArgumentError, "参数 #{key} 不能大于 #{max_val}"
-
end
-
end
-
-
# 长度验证
-
if rules[:length] && value.present?
-
min_len, max_len = rules[:length]
-
if min_len && value.to_s.length < min_len
-
raise ArgumentError, "参数 #{key} 长度不能小于 #{min_len}"
-
end
-
if max_len && value.to_s.length > max_len
-
raise ArgumentError, "参数 #{key} 长度不能大于 #{max_len}"
-
end
-
end
-
-
# 正则表达式验证
-
if rules[:format] && value.present?
-
regex = rules[:format].is_a?(Regexp) ? rules[:format] : Regexp.new(rules[:format])
-
unless value.to_s.match?(regex)
-
raise ArgumentError, "参数 #{key} 格式不正确"
-
end
-
end
-
-
# 枚举值验证
-
if rules[:in] && value.present?
-
unless rules[:in].include?(value)
-
raise ArgumentError, "参数 #{key} 必须是以下值之一: #{rules[:in].join(', ')}"
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ApiRateLimitingService - API限流服务
-
# 提供基于Redis的API请求限流功能
-
class ApiRateLimitingService < ApplicationService
-
include ServiceInterface
-
-
attr_reader :identifier, :limit, :window, :endpoint, :request
-
-
def initialize(identifier:, limit: 100, window: 1.hour, endpoint: nil, request: nil)
-
super()
-
@identifier = identifier
-
@limit = limit
-
@window = window
-
@endpoint = endpoint
-
@request = request
-
end
-
-
def call
-
handle_errors do
-
check_rate_limit
-
end
-
self
-
end
-
-
def allowed?
-
@allowed
-
end
-
-
def remaining_requests
-
@remaining_requests
-
end
-
-
def reset_time
-
@reset_time
-
end
-
-
def current_usage
-
@current_usage
-
end
-
-
# 类方法:检查用户限流
-
def self.check_user_rate_limit(user, endpoint: nil, request: nil)
-
identifier = "user:#{user.id}"
-
limit = rate_limit_for_user(user, endpoint)
-
window = rate_window_for_user(user, endpoint)
-
-
new(
-
identifier: identifier,
-
limit: limit,
-
window: window,
-
endpoint: endpoint,
-
request: request
-
).call
-
end
-
-
# 类方法:检查IP限流
-
def self.check_ip_rate_limit(ip_address, endpoint: nil, request: nil)
-
identifier = "ip:#{ip_address}"
-
limit = rate_limit_for_ip(ip_address, endpoint)
-
window = rate_window_for_ip(ip_address, endpoint)
-
-
new(
-
identifier: identifier,
-
limit: limit,
-
window: window,
-
endpoint: endpoint,
-
request: request
-
).call
-
end
-
-
# 类方法:检查全局限流
-
def self.check_global_rate_limit(endpoint: nil, request: nil)
-
identifier = "global"
-
limit = rate_limit_for_global(endpoint)
-
window = rate_window_for_global(endpoint)
-
-
new(
-
identifier: identifier,
-
limit: limit,
-
window: window,
-
endpoint: endpoint,
-
request: request
-
).call
-
end
-
-
private
-
-
def check_rate_limit
-
# 使用Redis滑动窗口算法
-
redis_key = build_redis_key
-
-
# 获取当前时间窗口内的请求计数
-
current_time = Time.current.to_f
-
window_start = current_time - window.to_f
-
-
# 清理过期的请求记录
-
cleanup_expired_requests(redis_key, window_start)
-
-
# 获取当前窗口内的请求数量
-
@current_usage = get_current_usage(redis_key)
-
@remaining_requests = [limit - @current_usage, 0].max
-
@reset_time = calculate_reset_time(redis_key)
-
-
# 检查是否超过限制
-
if @current_usage >= limit
-
@allowed = false
-
log_rate_limit_violation
-
add_error(:rate_limit_exceeded, "API请求频率超过限制,请稍后重试")
-
else
-
@allowed = true
-
record_request(redis_key, current_time)
-
end
-
end
-
-
def build_redis_key
-
key_parts = ['rate_limit', identifier]
-
key_parts << endpoint if endpoint
-
key_parts.join(':')
-
end
-
-
def cleanup_expired_requests(redis_key, window_start)
-
# 使用Redis有序集合,按时间戳存储请求
-
redis.zremrangebyscore(redis_key, 0, window_start)
-
end
-
-
def get_current_usage(redis_key)
-
redis.zcard(redis_key)
-
end
-
-
def record_request(redis_key, current_time)
-
# 记录当前请求
-
redis.zadd(redis_key, current_time, generate_request_id(current_time))
-
-
# 设置键的过期时间
-
redis.expire(redis_key, window.to_i)
-
end
-
-
def calculate_reset_time(redis_key)
-
# 获取最早的请求时间
-
earliest_request = redis.zrange(redis_key, 0, 0, withscores: true)
-
-
if earliest_request.any?
-
earliest_timestamp = earliest_request.first[1].to_f
-
reset_timestamp = earliest_timestamp + window.to_f
-
Time.at(reset_timestamp).iso8601
-
else
-
(Time.current + window).iso8601
-
end
-
end
-
-
def generate_request_id(current_time)
-
"#{current_time}_#{SecureRandom.hex(8)}"
-
end
-
-
def log_rate_limit_violation
-
Rails.logger.warn(
-
"Rate limit exceeded",
-
{
-
identifier: identifier,
-
endpoint: endpoint,
-
limit: limit,
-
window: window,
-
current_usage: @current_usage,
-
ip: request&.remote_ip,
-
user_agent: request&.user_agent
-
}
-
)
-
end
-
-
def redis
-
@redis ||= Rails.cache.redis || Redis.new
-
end
-
-
# 类方法:定义不同角色的限流规则
-
def self.rate_limit_for_user(user, endpoint = nil)
-
return 1000 if user&.admin? # 管理员:1000次/小时
-
return 500 if user&.vip? # VIP用户:500次/小时
-
return 200 if user&.premium? # 高级用户:200次/小时
-
100 # 普通用户:100次/小时
-
end
-
-
def self.rate_window_for_user(user, endpoint = nil)
-
return 5.minutes if user&.admin?
-
1.hour
-
end
-
-
def self.rate_limit_for_ip(ip_address, endpoint = nil)
-
# 检查是否为可信IP
-
return 1000 if trusted_ip?(ip_address)
-
50 # 默认:50次/分钟
-
end
-
-
def self.rate_window_for_ip(ip_address, endpoint = nil)
-
return 5.minutes if trusted_ip?(ip_address)
-
1.minute
-
end
-
-
def self.rate_limit_for_global(endpoint = nil)
-
case endpoint
-
when /auth/
-
20 # 认证相关:20次/分钟
-
when /upload/
-
10 # 上传相关:10次/分钟
-
else
-
1000 # 全局:1000次/秒
-
end
-
end
-
-
def self.rate_window_for_global(endpoint = nil)
-
case endpoint
-
when /auth/
-
1.minute
-
when /upload/
-
1.minute
-
else
-
1.second
-
end
-
end
-
-
def self.trusted_ip?(ip_address)
-
# 可信IP列表(可以来自配置或数据库)
-
trusted_ips = Rails.application.config.x.trusted_ips || []
-
trusted_ips.include?(ip_address)
-
end
-
-
# 获取限流统计信息
-
def self.rate_limit_stats(identifier, window = 1.hour)
-
redis_key = "rate_limit:#{identifier}"
-
current_time = Time.current.to_f
-
window_start = current_time - window.to_f
-
-
# 清理过期记录
-
Rails.cache.redis&.zremrangebyscore(redis_key, 0, window_start)
-
-
# 获取统计数据
-
{
-
current_usage: Rails.cache.redis&.zcard(redis_key) || 0,
-
requests_in_window: get_requests_in_window(redis_key, window_start, current_time),
-
peak_usage: get_peak_usage(redis_key),
-
average_usage: calculate_average_usage(redis_key, window)
-
}
-
end
-
-
def self.get_requests_in_window(redis_key, window_start, current_time)
-
requests = Rails.cache.redis&.zrangebyscore(redis_key, window_start, current_time) || []
-
requests.map { |req| req.split('_').first.to_f }
-
end
-
-
def self.get_peak_usage(redis_key)
-
# 这里可以实现更复杂的峰值统计逻辑
-
Rails.cache.redis&.zcard(redis_key) || 0
-
end
-
-
def self.calculate_average_usage(redis_key, window)
-
total_requests = Rails.cache.redis&.zcard(redis_key) || 0
-
(total_requests.to_f / window).round(2)
-
end
-
-
# 重置用户限流计数器
-
def self.reset_user_rate_limit(user_id)
-
pattern = "rate_limit:user:#{user_id}:*"
-
keys = Rails.cache.redis&.keys(pattern) || []
-
-
keys.each do |key|
-
Rails.cache.redis&.del(key)
-
end
-
end
-
-
# 重置IP限流计数器
-
def self.reset_ip_rate_limit(ip_address)
-
pattern = "rate_limit:ip:#{ip_address}:*"
-
keys = Rails.cache.redis&.keys(pattern) || []
-
-
keys.each do |key|
-
Rails.cache.redis&.del(key)
-
end
-
end
-
-
# 获取限流配置信息
-
def self.rate_limit_config
-
{
-
user_limits: {
-
admin: { limit: 1000, window: '5 minutes' },
-
vip: { limit: 500, window: '1 hour' },
-
premium: { limit: 200, window: '1 hour' },
-
regular: { limit: 100, window: '1 hour' }
-
},
-
ip_limits: {
-
trusted: { limit: 1000, window: '5 minutes' },
-
regular: { limit: 50, window: '1 minute' }
-
},
-
global_limits: {
-
auth: { limit: 20, window: '1 minute' },
-
upload: { limit: 10, window: '1 minute' },
-
default: { limit: 1000, window: '1 second' }
-
}
-
}
-
end
-
end
-
# frozen_string_literal: true
-
-
# ApiResponseService - API响应标准化服务
-
# 提供统一的API响应格式,包括成功响应、错误响应、分页响应等
-
class ApiResponseService
-
include ActionView::Helpers::NumberHelper
-
-
# 尝试加载 RequestStore,如果不存在则跳过
-
begin
-
require 'request_store'
-
rescue LoadError
-
# RequestStore gem 没有安装,使用简单的替代方案
-
end
-
-
class << self
-
# 标准成功响应
-
# @param data [Object] 响应数据
-
# @param message [String] 响应消息
-
# @param meta [Hash] 元数据
-
# @param status_code [Integer] HTTP状态码
-
# @return [Hash] 标准化的响应格式
-
def success_response(data: nil, message: '操作成功', meta: {}, status_code: 200)
-
response = {
-
success: true,
-
message: message,
-
data: data,
-
meta: standard_meta(meta),
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加请求ID(如果存在)
-
add_request_id(response)
-
-
[response, status_code]
-
end
-
-
# 标准错误响应
-
# @param message [String] 错误消息
-
# @param error_code [String] 错误代码
-
# @param details [Hash] 错误详情
-
# @param status_code [Integer] HTTP状态码
-
# @return [Hash] 标准化的错误响应格式
-
def error_response(message: '操作失败', error_code: nil, details: {}, status_code: 400)
-
response = {
-
success: false,
-
message: message,
-
error_code: error_code,
-
data: nil,
-
meta: standard_meta,
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加错误详情(开发环境)
-
if Rails.env.development? && details.any?
-
response[:details] = details
-
end
-
-
# 添加请求ID(如果存在)
-
add_request_id(response)
-
-
[response, status_code]
-
end
-
-
# 验证错误响应
-
# @param errors [ActiveModel::Errors] 验证错误对象
-
# @param message [String] 响应消息
-
# @return [Hash] 标准化的验证错误响应格式
-
def validation_error_response(errors, message: '请求参数验证失败')
-
error_details = if errors.respond_to?(:details)
-
errors.details.transform_values do |details|
-
details.map { |detail| detail[:error].to_s.humanize }
-
end
-
else
-
errors.is_a?(Hash) ? errors : { base: [errors.to_s] }
-
end
-
-
error_response(
-
message: message,
-
error_code: 'validation_error',
-
details: { errors: error_details },
-
status_code: 422
-
)
-
end
-
-
# 未找到错误响应
-
# @param resource_type [String] 资源类型
-
# @param resource_id [String, Integer] 资源ID
-
# @return [Hash] 标准化的未找到响应格式
-
def not_found_response(resource_type: '资源', resource_id: nil)
-
message = if resource_id
-
"#{resource_type} (ID: #{resource_id}) 不存在"
-
else
-
"#{resource_type} 不存在"
-
end
-
-
error_response(
-
message: message,
-
error_code: 'not_found',
-
status_code: 404
-
)
-
end
-
-
# 权限错误响应
-
# @param message [String] 错误消息
-
# @param required_permission [String] 需要的权限
-
# @return [Hash] 标准化的权限错误响应格式
-
def authorization_error_response(message: '权限不足', required_permission: nil)
-
details = {}
-
details[:required_permission] = required_permission if required_permission
-
-
error_response(
-
message: message,
-
error_code: 'authorization_error',
-
details: details,
-
status_code: 403
-
)
-
end
-
-
# 认证错误响应
-
# @param message [String] 错误消息
-
# @param details [Hash] 错误详情
-
# @return [Hash] 标准化的认证错误响应格式
-
def authentication_error_response(message: '认证失败', details: {})
-
error_response(
-
message: message,
-
error_code: 'authentication_error',
-
details: details,
-
status_code: 401
-
)
-
end
-
-
# 服务不可用错误响应
-
# @param service_name [String] 服务名称
-
# @param retry_after [Integer] 建议重试时间(秒)
-
# @return [Hash] 标准化的服务不可用响应格式
-
def service_unavailable_response(service_name: '服务', retry_after: 30)
-
message = "#{service_name}暂时不可用,请稍后再试"
-
-
response, = error_response(
-
message: message,
-
error_code: 'service_unavailable',
-
status_code: 503
-
)
-
-
# 添加重试信息
-
response[:meta][:retry_after] = retry_after
-
-
[response, 503]
-
end
-
-
# 限流错误响应
-
# @param limit_info [Hash] 限流信息
-
# @return [Hash] 标准化的限流错误响应格式
-
def rate_limit_error_response(limit_info = {})
-
message = '请求过于频繁,请稍后再试'
-
-
response, = error_response(
-
message: message,
-
error_code: 'rate_limit_exceeded',
-
status_code: 429
-
)
-
-
# 添加限流信息
-
response[:meta].merge!(limit_info) if limit_info.any?
-
-
[response, 429]
-
end
-
-
# 分页响应
-
# @param records [Array] 记录数组
-
# @param pagination [Hash] 分页信息
-
# @param message [String] 响应消息
-
# @param additional_meta [Hash] 额外的元数据
-
# @return [Hash] 标准化的分页响应格式
-
def paginated_response(records:, pagination:, message: '获取成功', additional_meta: {})
-
meta = standard_meta(pagination.merge(additional_meta))
-
-
success_response(
-
data: records,
-
message: message,
-
meta: meta
-
)
-
end
-
-
# 创建成功响应
-
# @param resource [Object] 创建的资源
-
# @param resource_name [String] 资源名称
-
# @return [Hash] 标准化的创建成功响应格式
-
def create_success_response(resource, resource_name: '资源')
-
message = "#{resource_name}创建成功"
-
-
success_response(
-
data: resource,
-
message: message,
-
status_code: 201
-
)
-
end
-
-
# 更新成功响应
-
# @param resource [Object] 更新的资源
-
# @param resource_name [String] 资源名称
-
# @return [Hash] 标准化的更新成功响应格式
-
def update_success_response(resource, resource_name: '资源')
-
message = "#{resource_name}更新成功"
-
-
success_response(
-
data: resource,
-
message: message
-
)
-
end
-
-
# 删除成功响应
-
# @param resource_name [String] 资源名称
-
# @return [Hash] 标准化的删除成功响应格式
-
def destroy_success_response(resource_name: '资源')
-
message = "#{resource_name}删除成功"
-
-
success_response(
-
data: nil,
-
message: message
-
)
-
end
-
-
# 批量操作响应
-
# @param results [Hash] 批量操作结果
-
# @param operation_name [String] 操作名称
-
# @return [Hash] 标准化的批量操作响应格式
-
def batch_operation_response(results, operation_name: '批量操作')
-
successful_count = results[:successful]&.count || 0
-
failed_count = results[:failed]&.count || 0
-
total_count = results[:total] || successful_count + failed_count
-
-
if failed_count == 0
-
message = "#{operation_name}全部成功 (#{successful_count}/#{total_count})"
-
elsif successful_count == 0
-
message = "#{operation_name}全部失败 (0/#{total_count})"
-
else
-
message = "#{operation_name}部分成功 (#{successful_count}/#{total_count})"
-
end
-
-
success_response(
-
data: results,
-
message: message,
-
meta: {
-
successful_count: successful_count,
-
failed_count: failed_count,
-
total_count: total_count,
-
success_rate: total_count > 0 ? (successful_count.to_f / total_count * 100).round(1) : 0
-
}
-
)
-
end
-
-
# 健康检查响应
-
# @param additional_info [Hash] 额外的健康信息
-
# @return [Hash] 健康检查响应格式
-
def health_response(additional_info = {})
-
health_data = {
-
status: 'healthy',
-
timestamp: Time.current.iso8601,
-
environment: Rails.env,
-
version: Rails.application.config.version || '1.0.0',
-
uptime: number_to_human(Time.current - Rails.application.booted_at),
-
memory_usage: number_to_human_size(`ps -o rss= -p #{Process.pid}`.to_i)
-
}
-
-
health_data.merge!(additional_info) if additional_info.any?
-
-
success_response(
-
data: health_data,
-
message: '服务运行正常'
-
)
-
end
-
-
private
-
-
# 标准化元数据
-
# @param meta [Hash] 原始元数据
-
# @return [Hash] 标准化的元数据
-
def standard_meta(meta = {})
-
{
-
version: (Rails.application.config.api_version rescue nil) || 'v1',
-
server_time: Time.current.iso8601
-
}.merge(meta)
-
end
-
-
# 添加请求ID到响应中
-
# @param response [Hash] 响应对象
-
def add_request_id(response)
-
request_id = if defined?(RequestStore)
-
RequestStore.store[:request_id]
-
else
-
Thread.current[:request_id]
-
end
-
response[:request_id] = request_id if request_id
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ApiVersionService - API版本控制服务
-
# 提供API版本管理、兼容性处理和版本信息查询
-
class ApiVersionService
-
# 当前支持的API版本
-
SUPPORTED_VERSIONS = %w[v1].freeze
-
-
# 默认API版本
-
DEFAULT_VERSION = 'v1'.freeze
-
-
# 版本弃用时间(天)
-
DEPRECATION_DAYS = 90.freeze
-
-
class << self
-
# 从请求中确定API版本
-
# @param request [ActionDispatch::Request] HTTP请求对象
-
# @param available_versions [Array] 可用的版本列表
-
# @return [String] 确定的API版本
-
def determine_api_version(request, available_versions = SUPPORTED_VERSIONS)
-
# 1. 从URL路径获取版本
-
version_from_path = extract_version_from_path(request.path)
-
return version_from_path if available_versions.include?(version_from_path)
-
-
# 2. 从请求头获取版本
-
version_from_header = request.headers['API-Version'] || request.headers['X-API-Version']
-
return version_from_header if available_versions.include?(version_from_header)
-
-
# 3. 从查询参数获取版本
-
version_from_params = request.params['api_version'] || request.params['version']
-
return version_from_params if available_versions.include?(version_from_params)
-
-
# 4. 返回默认版本
-
DEFAULT_VERSION
-
end
-
-
# 检查版本是否支持
-
# @param version [String] 版本号
-
# @param available_versions [Array] 可用的版本列表
-
# @return [Boolean] 是否支持
-
def version_supported?(version, available_versions = SUPPORTED_VERSIONS)
-
available_versions.include?(version)
-
end
-
-
# 获取版本信息
-
# @param version [String] 版本号
-
# @return [Hash] 版本信息
-
def version_info(version = DEFAULT_VERSION)
-
version_configs = {
-
'v1' => {
-
version: 'v1',
-
name: 'QQClub API v1.0',
-
description: 'QQClub读书会平台API的第一个稳定版本',
-
release_date: '2025-10-15',
-
status: 'stable',
-
deprecated: false,
-
sunset_date: nil,
-
features: [
-
'用户认证和授权',
-
'共读活动管理',
-
'打卡和进度跟踪',
-
'小红花激励机制',
-
'评论和互动系统',
-
'通知系统',
-
'内容搜索和导出',
-
'数据统计分析'
-
],
-
endpoints: {
-
auth: [
-
'POST /api/auth/mock_login',
-
'POST /api/auth/wechat_login',
-
'POST /api/auth/refresh_token',
-
'GET /api/auth/me'
-
],
-
events: [
-
'GET /api/v1/reading_events',
-
'POST /api/v1/reading_events',
-
'GET /api/v1/reading_events/:id',
-
'PUT /api/v1/reading_events/:id',
-
'DELETE /api/v1/reading_events/:id'
-
],
-
check_ins: [
-
'POST /api/v1/check_ins',
-
'GET /api/v1/check_ins',
-
'GET /api/v1/check_ins/:id'
-
],
-
flowers: [
-
'POST /api/v1/flowers',
-
'GET /api/v1/flowers',
-
'GET /api/v1/flower_leaderboards'
-
],
-
notifications: [
-
'GET /api/v1/notifications',
-
'POST /api/v1/notifications/mark_all_read'
-
],
-
analytics: [
-
'GET /api/v1/analytics/overview',
-
'GET /api/v1/analytics/dashboard'
-
]
-
}
-
}
-
}
-
-
version_configs[version] || {
-
version: version,
-
name: "Unknown Version",
-
description: "版本信息未知",
-
status: 'unknown',
-
deprecated: false
-
}
-
end
-
-
# 获取所有版本信息
-
# @return [Array] 所有版本的详细信息
-
def all_versions_info
-
SUPPORTED_VERSIONS.map { |version| version_info(version) }
-
end
-
-
# 检查版本是否已弃用
-
# @param version [String] 版本号
-
# @return [Boolean] 是否已弃用
-
def version_deprecated?(version)
-
version_info(version)[:deprecated]
-
end
-
-
# 获取版本弃用信息
-
# @param version [String] 版本号
-
# @return [Hash] 弃用信息
-
def deprecation_info(version)
-
info = version_info(version)
-
-
if info[:deprecated]
-
{
-
version: version,
-
deprecated: true,
-
sunset_date: info[:sunset_date],
-
migration_guide: info[:migration_guide],
-
alternative_versions: SUPPORTED_VERSIONS.reject { |v| v == version }
-
}
-
else
-
{
-
version: version,
-
deprecated: false
-
}
-
end
-
end
-
-
# 创建版本响应头
-
# @param version [String] 当前版本
-
# @param response_headers [Hash] 响应头
-
# @return [Hash] 更新后的响应头
-
def create_version_headers(version, response_headers = {})
-
headers = response_headers.dup
-
-
# API版本信息
-
headers['API-Version'] = version
-
headers['Supported-Versions'] = SUPPORTED_VERSIONS.join(',')
-
-
# 如果版本已弃用,添加弃用警告
-
if version_deprecated?(version)
-
headers['Deprecation'] = 'true'
-
headers['Sunset'] = version_info(version)[:sunset_date] if version_info(version)[:sunset_date]
-
headers['Migration-Guide'] = version_info(version)[:migration_guide] if version_info(version)[:migration_guide]
-
end
-
-
headers
-
end
-
-
# 生成版本弃用通知
-
# @param version [String] 弃用的版本
-
# @return [Hash] 弃用通知信息
-
def generate_deprecation_warning(version)
-
info = version_info(version)
-
-
{
-
warning: "API版本 #{version} 已弃用",
-
message: "请升级到更新的API版本以获得更好的服务和功能",
-
sunset_date: info[:sunset_date],
-
days_until_sunset: info[:sunset_date] ? ((Date.parse(info[:sunset_date]) - Date.current).to_i) : nil,
-
migration_guide: info[:migration_guide],
-
recommended_version: DEFAULT_VERSION,
-
supported_versions: SUPPORTED_VERSIONS.reject { |v| v == version }
-
}
-
end
-
-
# 验证版本兼容性
-
# @param requested_version [String] 请求的版本
-
# @param available_versions [Array] 可用版本
-
# @return [Hash] 兼容性检查结果
-
def check_version_compatibility(requested_version, available_versions = SUPPORTED_VERSIONS)
-
result = {
-
requested_version: requested_version,
-
compatible: false,
-
supported: false,
-
deprecated: false,
-
recommended_version: DEFAULT_VERSION,
-
messages: []
-
}
-
-
# 检查版本是否支持
-
if version_supported?(requested_version, available_versions)
-
result[:supported] = true
-
result[:compatible] = true
-
-
# 检查是否已弃用
-
if version_deprecated?(requested_version)
-
result[:deprecated] = true
-
result[:messages] << "版本 #{requested_version} 已弃用,建议升级到 #{DEFAULT_VERSION}"
-
-
deprecation_info = generate_deprecation_warning(requested_version)
-
result[:deprecation_warning] = deprecation_info
-
end
-
else
-
result[:messages] << "不支持的API版本: #{requested_version}"
-
result[:messages] << "支持的版本: #{available_versions.join(', ')}"
-
result[:messages] << "建议使用版本: #{DEFAULT_VERSION}"
-
end
-
-
result
-
end
-
-
# 从URL路径中提取版本号
-
# @param path [String] URL路径
-
# @return [String, nil] 版本号
-
def extract_version_from_path(path)
-
return nil unless path
-
-
# 匹配 /api/v1/ 格式
-
match = path.match(%r{/api/(v\d+)/})
-
match ? match[1] : nil
-
end
-
-
# 获取版本变更日志
-
# @param version [String] 版本号
-
# @return [Array] 变更日志
-
def changelog(version = DEFAULT_VERSION)
-
changelog_data = {
-
'v1' => [
-
{
-
date: '2025-10-15',
-
version: 'v1.0.0',
-
type: 'release',
-
description: 'QQClub API v1.0 正式发布',
-
changes: [
-
'实现完整的用户认证和授权系统',
-
'提供共读活动管理功能',
-
'支持打卡和进度跟踪',
-
'引入小红花激励机制',
-
'添加评论和互动系统',
-
'实现通知系统',
-
'提供内容搜索和导出功能',
-
'集成数据统计分析'
-
],
-
breaking_changes: [],
-
new_features: [
-
'POST /api/v1/reading_events - 创建共读活动',
-
'POST /api/v1/check_ins - 提交打卡',
-
'POST /api/v1/flowers - 发送小红花',
-
'GET /api/v1/notifications - 获取通知列表',
-
'GET /api/v1/analytics/overview - 获取统计概览'
-
]
-
}
-
]
-
}
-
-
changelog_data[version] || []
-
end
-
-
# 比较版本
-
# @param version1 [String] 版本1
-
# @param version2 [String] 版本2
-
# @return [Integer] 比较结果 (-1, 0, 1)
-
def compare_versions(version1, version2)
-
v1_parts = version1.scan(/\d+/).map(&:to_i)
-
v2_parts = version2.scan(/\d+/).map(&:to_i)
-
-
max_length = [v1_parts.length, v2_parts.length].max
-
-
max_length.times do |i|
-
v1_part = v1_parts[i] || 0
-
v2_part = v2_parts[i] || 0
-
-
comparison = v1_part <=> v2_part
-
return comparison unless comparison == 0
-
end
-
-
0
-
end
-
-
# 获取最新的稳定版本
-
# @return [String] 最新版本
-
def latest_stable_version
-
SUPPORTED_VERSIONS.select { |v| version_info(v)[:status] == 'stable' }
-
.max { |a, b| compare_versions(a, b) }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ApplicationService - 所有Service的基类
-
# 提供统一的错误处理、成功/失败状态管理
-
1
class ApplicationService
-
1
include ActiveModel::Model
-
-
1
attr_reader :errors, :result
-
-
1
def initialize
-
3
@errors = []
-
3
@success = false
-
3
@result = nil
-
end
-
-
# 子类必须实现call方法
-
1
def call
-
raise NotImplementedError, "子类必须实现call方法"
-
end
-
-
# 标记操作成功
-
1
def success!(result = nil)
-
4
@success = true
-
4
@result = result
-
end
-
-
# 标记操作失败
-
1
def failure!(error_messages)
-
1
@success = false
-
1
@errors = Array(error_messages)
-
end
-
-
# 检查操作是否成功
-
1
def success?
-
@success
-
end
-
-
# 检查操作是否失败
-
1
def failure?
-
!@success
-
end
-
-
# 添加错误信息
-
1
def add_error(message)
-
@errors << message
-
end
-
-
# 检查是否有错误
-
1
def errors?
-
@errors.any?
-
end
-
-
# 获取第一个错误信息
-
1
def first_error
-
@errors.first
-
end
-
-
# 获取所有错误信息
-
1
def error_messages
-
@errors
-
end
-
-
# 获取错误信息(别名)
-
1
def error_message
-
@errors.first
-
end
-
-
# 清空错误信息
-
1
def clear_errors!
-
@errors = []
-
end
-
-
1
private
-
-
# 块执行 - 统一异常处理
-
1
def handle_errors
-
3
yield
-
rescue => e
-
1
Rails.logger.error "Service Error: #{e.message}"
-
1
Rails.logger.error e.backtrace.join("\n")
-
1
failure!("系统错误: #{e.message}")
-
end
-
-
# 验证必需参数
-
1
def require_params!(params, required_keys)
-
missing_keys = required_keys.select { |key| params[key].blank? }
-
if missing_keys.any?
-
failure!("缺少必需参数: #{missing_keys.join(', ')}")
-
return false
-
end
-
true
-
end
-
-
# 验证对象存在
-
1
def require_record!(record, error_message = "记录不存在")
-
unless record
-
failure!(error_message)
-
return false
-
end
-
true
-
end
-
end
-
# frozen_string_literal: true
-
-
# AuthenticationService - 用户认证逻辑服务
-
# 负责微信登录、模拟登录、用户创建/查找等业务逻辑
-
class AuthenticationService < ApplicationService
-
attr_reader :login_params, :user, :login_type
-
-
def initialize(login_params: {}, login_type: :mock)
-
super()
-
@login_params = login_params
-
@login_type = login_type
-
@user = nil
-
end
-
-
# 主要调用方法
-
def call
-
handle_errors do
-
case login_type
-
when :mock
-
mock_login
-
when :wechat
-
wechat_login
-
else
-
failure!("不支持的登录类型: #{login_type}")
-
end
-
end
-
self # 返回service实例
-
end
-
-
# 类方法:模拟登录
-
def self.mock_login!(params = {})
-
new(login_params: params, login_type: :mock).call
-
end
-
-
# 类方法:微信登录
-
def self.wechat_login!(params = {})
-
new(login_params: params, login_type: :wechat).call
-
end
-
-
private
-
-
# 模拟登录逻辑
-
def mock_login
-
# 处理嵌套 JSON 参数或平铺参数
-
# 优先使用顶级参数,如果没有则使用嵌套的user参数
-
openid = login_params[:openid] || login_params.dig(:user, :wx_openid) || login_params.dig(:user, :openid) || "test_dhh_001"
-
nickname = login_params[:nickname] || login_params.dig(:user, :nickname) || "DHH"
-
avatar_url = login_params[:avatar_url] || login_params.dig(:user, :avatar_url)
-
-
# 查找或创建用户
-
@user = User.find_or_create_by(wx_openid: openid) do |u|
-
u.nickname = nickname
-
# 如果没有提供头像,生成一个随机头像
-
u.avatar_url = avatar_url.presence || AvatarGeneratorService.generate_themed_avatar(
-
nickname: nickname,
-
user_id: openid
-
)
-
end
-
-
# 如果用户已存在但没有头像,也生成一个
-
if @user.avatar_url.blank? || @user.avatar_url.include?('example.com/avatar.jpg')
-
@user.update!(
-
avatar_url: AvatarGeneratorService.generate_themed_avatar(
-
nickname: @user.nickname,
-
user_id: @user.id
-
)
-
)
-
end
-
-
generate_token_response
-
end
-
-
# 微信登录逻辑
-
def wechat_login
-
code = login_params[:code]
-
return failure!("缺少 code 参数") unless code
-
-
# 获取小程序传递的用户信息
-
user_info = login_params[:user_info] || {}
-
openid = login_params[:openid] || user_info[:openid]
-
unionid = login_params[:unionid] || user_info[:unionid]
-
nickname = login_params[:nickname] || user_info[:nickname]
-
avatar_url = login_params[:avatar_url] || login_params[:avatarUrl] || user_info[:avatar_url] || user_info[:avatarUrl]
-
-
# 如果没有直接提供用户信息,尝试调用微信API获取
-
if openid.blank? && code.present?
-
wechat_result = fetch_wechat_openid(code)
-
-
# 如果是测试环境且有用户信息,生成测试openid
-
if wechat_result.nil? && user_info.present?
-
openid = "test_wechat_#{Time.current.to_i}_#{rand(1000)}"
-
Rails.logger.info "使用测试openid: #{openid} for user: #{user_info[:nickname]}"
-
else
-
return failure!("微信登录失败") unless wechat_result
-
openid = wechat_result[:openid]
-
unionid = wechat_result[:unionid]
-
end
-
end
-
-
return failure!("无法获取用户标识") unless openid
-
-
# 查找或创建用户
-
@user = User.find_or_create_by(wx_openid: openid) do |u|
-
u.wx_unionid = unionid if unionid.present?
-
u.nickname = nickname.presence || "用户#{rand(1000..9999)}"
-
# 优先使用微信提供的真实头像,如果没有则生成默认头像
-
u.avatar_url = if avatar_url.present?
-
avatar_url
-
else
-
AvatarGeneratorService.generate_themed_avatar(
-
nickname: u.nickname,
-
user_id: openid
-
)
-
end
-
end
-
-
# 如果用户已存在但头像为空或使用了默认头像,且当前提供了新头像,则更新
-
if avatar_url.present? && (@user.avatar_url.blank? || @user.avatar_url.include?('example.com/avatar.jpg'))
-
update_attrs = { avatar_url: avatar_url }
-
update_attrs[:nickname] = nickname if nickname.present?
-
@user.update!(update_attrs)
-
end
-
-
generate_token_response
-
end
-
-
# 生成Token响应
-
def generate_token_response
-
access_token = @user.generate_jwt_token
-
refresh_token = @user.generate_refresh_token
-
-
response_data = {
-
access_token: access_token,
-
refresh_token: refresh_token,
-
user: user_data(@user)
-
}
-
-
success!(response_data)
-
end
-
-
# 格式化用户数据 - 返回字符串键格式用于API响应
-
def user_data(user)
-
{
-
'id' => user.id,
-
'nickname' => user.nickname,
-
'wx_openid' => user.wx_openid,
-
'avatar_url' => user.avatar_url,
-
'phone' => user.phone
-
}
-
end
-
-
# 调用微信API获取openid(简化版本)
-
def fetch_wechat_openid(code)
-
# TODO: 配置 credentials 后实现
-
# app_id = Rails.application.credentials.wechat[:app_id]
-
# app_secret = Rails.application.credentials.wechat[:app_secret]
-
# url = "https://api.weixin.qq.com/sns/jscode2session"
-
# response = HTTParty.get(url, query: {
-
# appid: app_id,
-
# secret: app_secret,
-
# js_code: code,
-
# grant_type: "authorization_code"
-
# })
-
#
-
# if response["openid"]
-
# { openid: response["openid"], unionid: response["unionid"] }
-
# else
-
# nil
-
# end
-
-
# 暂时返回 nil,提示用户配置 credentials
-
nil
-
end
-
end
-
# frozen_string_literal: true
-
-
# AvatarGeneratorService - 生成随机头像服务
-
# 使用 Picsum Photos API 生成随机头像
-
class AvatarGeneratorService < ApplicationService
-
# 可用的头像主题和参数
-
AVATAR_THEMES = %w[
-
abstract animals architecture business cats city fashion
-
food nature nightlife people sport technology transport
-
].freeze
-
-
# 头像尺寸
-
AVATAR_SIZES = [100, 200, 300, 400, 500].freeze
-
-
# 生成用户头像URL
-
def self.generate_user_avatar(user_id: nil, size: 200)
-
# 基于用户ID生成一致的随机头像
-
seed = user_id ? "user_#{user_id}" : "user_#{Time.current.to_i}_#{rand(1000)}"
-
theme = AVATAR_THEMES.sample
-
width = height = size
-
-
# 使用 Picsum Photos API 生成随机头像
-
"https://picsum.photos/seed/#{seed}/#{width}/#{height}.jpg"
-
end
-
-
# 生成动物头像(适合可爱风格)
-
def self.generate_cute_avatar(user_id: nil)
-
seed = user_id ? "cute_#{user_id}" : "cute_#{Time.current.to_i}_#{rand(1000)}"
-
"https://picsum.photos/seed/#{seed}/200/200.jpg"
-
end
-
-
# 生成风景头像(适合通用风格)
-
def self.generate_nature_avatar(user_id: nil)
-
seed = user_id ? "nature_#{user_id}" : "nature_#{Time.current.to_i}_#{rand(1000)}"
-
"https://picsum.photos/seed/#{seed}/200/200.jpg"
-
end
-
-
# 生成几何头像(适合现代风格)
-
def self.generate_geometric_avatar(user_id: nil)
-
seed = user_id ? "geo_#{user_id}" : "geo_#{Time.current.to_i}_#{rand(1000)}"
-
"https://picsum.photos/seed/#{seed}/200/200.jpg"
-
end
-
-
# 根据用户昵称生成主题相关的头像
-
def self.generate_themed_avatar(nickname: nil, user_id: nil)
-
return generate_user_avatar(user_id: user_id) unless nickname
-
-
# 根据昵称关键词选择主题
-
theme_keywords = {
-
'猫' => 'cats',
-
'狗' => 'animals',
-
'花' => 'nature',
-
'树' => 'nature',
-
'山' => 'nature',
-
'海' => 'nature',
-
'书' => 'business',
-
'美食' => 'food',
-
'运动' => 'sport',
-
'音乐' => 'abstract',
-
'科技' => 'technology',
-
'城市' => 'city',
-
'时尚' => 'fashion'
-
}
-
-
selected_theme = 'nature' # 默认主题
-
theme_keywords.each do |keyword, theme|
-
if nickname.include?(keyword)
-
selected_theme = theme
-
break
-
end
-
end
-
-
seed = user_id ? "user_#{user_id}" : "user_#{Time.current.to_i}_#{rand(1000)}"
-
"https://picsum.photos/seed/#{seed}_#{selected_theme}/200/200.jpg"
-
end
-
-
# 获取默认头像列表(用于测试)
-
def self.default_avatars
-
[
-
'https://picsum.photos/seed/avatar1/200/200.jpg',
-
'https://picsum.photos/seed/avatar2/200/200.jpg',
-
'https://picsum.photos/seed/avatar3/200/200.jpg',
-
'https://picsum.photos/seed/avatar4/200/200.jpg',
-
'https://picsum.photos/seed/avatar5/200/200.jpg'
-
]
-
end
-
end
-
# frozen_string_literal: true
-
-
# 缓存服务
-
# 提供统一的缓存接口,支持多种缓存策略和数据类型
-
class CacheService
-
class << self
-
# 缓存用户基本信息
-
# @param user [User] 用户对象
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 用户基本信息
-
def cache_user_profile(user, ttl: 30.minutes)
-
cache_key = "user_profile:#{user.id}"
-
-
cached_data = Rails.cache.fetch(cache_key, expires_in: ttl) do
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url,
-
role: user.role_as_string,
-
created_at: user.created_at,
-
stats: {
-
events_count: user.created_events.count,
-
check_ins_count: user.check_ins.count,
-
flowers_given: user.given_flowers.count,
-
flowers_received: user.received_flowers.count
-
}
-
}
-
end
-
-
cached_data
-
end
-
-
# 缓存活动基本信息
-
# @param event [ReadingEvent] 活动对象
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 活动基本信息
-
def cache_event_profile(event, ttl: 30.minutes)
-
cache_key = "event_profile:#{event.id}"
-
-
cached_data = Rails.cache.fetch(cache_key, expires_in: ttl) do
-
{
-
id: event.id,
-
title: event.title,
-
book_name: event.book_name,
-
book_cover_url: event.book_cover_url,
-
description: event.description&.truncate(200),
-
status: event.status,
-
approval_status: event.approval_status,
-
start_date: event.start_date,
-
end_date: event.end_date,
-
max_participants: event.max_participants,
-
leader: event.leader&.as_json_for_api,
-
stats: {
-
enrolled_count: event.event_enrollments.where(status: 'enrolled').count,
-
check_ins_count: event.check_ins.count,
-
flowers_count: event.flowers_count
-
}
-
}
-
end
-
-
cached_data
-
end
-
-
# 缓存排行榜数据
-
# @param type [Symbol] 排行榜类型 (:flowers, :check_ins, :participation)
-
# @param period [Symbol] 时间周期 (:today, :week, :month, :all_time)
-
# @param limit [Integer] 返回记录数
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Array] 排行榜数据
-
def cache_leaderboard(type, period, limit: 10, ttl: 5.minutes)
-
cache_key = "leaderboard:#{type}:#{period}:#{limit}"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
case type
-
when :flowers
-
AnalyticsService.leaderboards(:flowers, limit, period)
-
when :check_ins
-
AnalyticsService.leaderboards(:check_ins, limit, period)
-
when :participation
-
AnalyticsService.leaderboards(:participation, limit, period)
-
else
-
[]
-
end
-
end
-
end
-
-
# 缓存用户统计信息
-
# @param user [User] 用户对象
-
# @param days [Integer] 统计天数
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 用户统计信息
-
def cache_user_analytics(user, days: 30, ttl: 10.minutes)
-
cache_key = "user_analytics:#{user.id}:#{days}days"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
AnalyticsService.user_analytics(user, days)
-
end
-
end
-
-
# 缓存系统统计信息
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 系统统计信息
-
def cache_system_overview(ttl: 1.hour)
-
cache_key = "system_overview"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
AnalyticsService.system_overview
-
end
-
end
-
-
# 缓存用户的未读通知数量
-
# @param user [User] 用户对象
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Integer] 未读通知数量
-
def cache_unread_notifications_count(user, ttl: 1.minute)
-
cache_key = "unread_notifications:#{user.id}"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
user.received_notifications.unread.count
-
end
-
end
-
-
# 缓存用户的活动报名状态
-
# @param user [User] 用户对象
-
# @param event [ReadingEvent] 活动对象
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 报名状态信息
-
def cache_event_enrollment_status(user, event, ttl: 5.minutes)
-
cache_key = "enrollment_status:#{user.id}:#{event.id}"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
enrollment = event.event_enrollments.find_by(user: user)
-
-
{
-
enrolled: enrollment.present?,
-
status: enrollment&.status,
-
enrollment_date: enrollment&.created_at,
-
can_enroll: event.can_enroll?,
-
is_full: event.full?,
-
check_ins_count: enrollment&.check_ins_count || 0,
-
completion_rate: enrollment&.completion_rate || 0
-
}
-
end
-
end
-
-
# 缓存今日小红花配额信息
-
# @param user [User] 用户对象
-
# @param event [ReadingEvent] 活动对象
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 配额信息
-
def cache_flower_quota_info(user, event, ttl: 1.minute)
-
cache_key = "flower_quota:#{user.id}:#{event.id}:#{Date.current}"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
FlowerQuotaService.get_daily_quota(user, event, Date.current)
-
end
-
end
-
-
# 批量缓存用户基本信息
-
# @param users [Array<User>] 用户数组
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 用户ID到缓存的映射
-
def batch_cache_user_profiles(users, ttl: 30.minutes)
-
return {} if users.empty?
-
-
# 批量查找需要缓存的用户
-
user_ids = users.map(&:id)
-
existing_cache_keys = user_ids.map { |id| "user_profile:#{id}" }
-
cached_data = Rails.cache.read_multi(*existing_cache_keys)
-
-
# 找出需要重新缓存的用户
-
uncached_users = users.reject { |user| cached_data.key?("user_profile:#{user.id}") }
-
-
# 批量缓存未缓存的用户
-
uncached_users.each do |user|
-
cache_user_profile(user, ttl: ttl)
-
end
-
-
# 返回所有用户的缓存数据
-
user_ids.index_with do |user_id|
-
Rails.cache.read("user_profile:#{user_id}")
-
end.to_h
-
end
-
-
# 缓存搜索结果
-
# @param search_term [String] 搜索关键词
-
# @param search_type [Symbol] 搜索类型
-
# @param results [Array] 搜索结果
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Array] 缓存的搜索结果
-
def cache_search_results(search_term, search_type, results, ttl: 15.minutes)
-
return results if search_term.blank? || results.empty?
-
-
cache_key = "search:#{search_type}:#{Digest::MD5.hexdigest(search_term.downcase)}"
-
-
Rails.cache.write(cache_key, results, expires_in: ttl)
-
results
-
end
-
-
# 获取缓存的搜索结果
-
# @param search_term [String] 搜索关键词
-
# @param search_type [Symbol] 搜索类型
-
# @return [Array, nil] 缓存的搜索结果或nil
-
def get_cached_search_results(search_term, search_type)
-
return nil if search_term.blank?
-
-
cache_key = "search:#{search_type}:#{Digest::MD5.hexdigest(search_term.downcase)}"
-
Rails.cache.read(cache_key)
-
end
-
-
# 缓存热门关键词
-
# @param keywords [Array<String>] 关键词数组
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Array<String>] 热门关键词
-
def cache_popular_keywords(keywords, ttl: 1.hour)
-
cache_key = "popular_keywords"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
keywords.first(10) # 只保留前10个
-
end
-
end
-
-
# 缓存配置信息
-
# @param ttl [Integer] 缓存时间(秒)
-
# @return [Hash] 配置信息
-
def cache_app_config(ttl: 1.hour)
-
cache_key = "app_config"
-
-
Rails.cache.fetch(cache_key, expires_in: ttl) do
-
{
-
max_flowers_per_check_in: 3,
-
max_check_in_length: 2000,
-
min_check_in_length: 50,
-
default_event_duration: 30.days,
-
max_event_participants: 100,
-
flower_quota_daily: 3,
-
notification_unread_limit: 50
-
}
-
end
-
end
-
-
# 清除用户相关的缓存
-
# @param user [User] 用户对象
-
def clear_user_cache(user)
-
cache_patterns = [
-
"user_profile:#{user.id}",
-
"user_analytics:#{user.id}:*",
-
"unread_notifications:#{user.id}",
-
"enrollment_status:#{user.id}:*",
-
"flower_quota:#{user.id}:*"
-
]
-
-
cache_patterns.each do |pattern|
-
if pattern.include?('*')
-
Rails.cache.delete_matched(pattern)
-
else
-
Rails.cache.delete(pattern)
-
end
-
end
-
end
-
-
# 清除活动相关的缓存
-
# @param event [ReadingEvent] 活动对象
-
def clear_event_cache(event)
-
cache_patterns = [
-
"event_profile:#{event.id}",
-
"enrollment_status:*:#{event.id}",
-
"leaderboard:*:*:*" # 清除所有排行榜缓存
-
]
-
-
cache_patterns.each do |pattern|
-
if pattern.include?('*')
-
Rails.cache.delete_matched(pattern)
-
else
-
Rails.cache.delete(pattern)
-
end
-
end
-
end
-
-
# 清除系统统计缓存
-
def clear_system_cache
-
Rails.cache.delete_matched("system_overview")
-
Rails.cache.delete_matched("leaderboard:*")
-
Rails.cache.delete_matched("popular_keywords")
-
end
-
-
# 预热缓存
-
# 预加载常用数据到缓存中
-
def warm_up_cache
-
# 缓存系统概览
-
cache_system_overview
-
-
# 缓存热门排行榜
-
[:flowers, :check_ins].each do |type|
-
[:today, :week, :month].each do |period|
-
cache_leaderboard(type, period)
-
end
-
end
-
-
# 缓存应用配置
-
cache_app_config
-
-
Rails.logger.info "缓存预热完成"
-
end
-
-
# 获取缓存统计信息
-
# @return [Hash] 缓存统计
-
def cache_stats
-
if Rails.cache.respond_to?(:stats)
-
Rails.cache.stats
-
else
-
{
-
cache_store: Rails.cache.class.name,
-
message: "当前缓存存储不支持统计功能"
-
}
-
end
-
end
-
-
# 检查缓存健康状态
-
# @return [Hash] 健康状态
-
def cache_health_check
-
test_key = "health_check_#{Time.current.to_i}"
-
test_value = { test: true, timestamp: Time.current }
-
-
begin
-
# 写入测试
-
Rails.cache.write(test_key, test_value, expires_in: 1.minute)
-
-
# 读取测试
-
cached_value = Rails.cache.read(test_key)
-
-
# 清理测试数据
-
Rails.cache.delete(test_key)
-
-
{
-
status: cached_value == test_value ? "healthy" : "unhealthy",
-
cache_store: Rails.cache.class.name,
-
test_time: Time.current
-
}
-
rescue => e
-
{
-
status: "error",
-
cache_store: Rails.cache.class.name,
-
error: e.message,
-
test_time: Time.current
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ServiceInterface - 服务接口规范模块
-
# 提供统一的服务接口和数据处理方法
-
module ServiceInterface
-
extend ActiveSupport::Concern
-
-
# 统一的数据访问方法
-
def data
-
@result
-
end
-
-
# 获取服务状态信息
-
def status_info
-
{
-
success: success?,
-
failure: failure?,
-
errors: error_messages,
-
has_errors: errors?,
-
error_count: error_messages.count,
-
first_error: first_error
-
}
-
end
-
-
# 安全的数据获取(失败时返回默认值)
-
def safe_data(default_value = nil)
-
success? ? data : default_value
-
end
-
-
# 检查服务是否可用于当前用户
-
def available_for_user?(user = nil)
-
# 默认实现,子类可以重写
-
true
-
end
-
-
# 获取服务类型标识
-
def service_type
-
self.class.name
-
end
-
-
# 获取服务描述
-
def service_description
-
self.class.name.demodulize.gsub(/Service$/, '')
-
end
-
-
# 批量操作结果格式化
-
def format_batch_results(results, operation_name: '批量操作')
-
successful_count = results.count { |r| r[:success] }
-
failed_count = results.count - successful_count
-
total_count = results.count
-
-
{
-
operation: operation_name,
-
success: successful_count == total_count,
-
summary: {
-
total: total_count,
-
successful: successful_count,
-
failed: failed_count,
-
success_rate: total_count > 0 ? (successful_count.to_f / total_count * 100).round(2) : 0
-
},
-
results: results
-
}
-
end
-
-
protected
-
-
# 验证用户权限
-
def authorize_user!(user, required_permission = nil)
-
return failure!("用户不能为空") unless user
-
return failure!("用户不存在") unless user.persisted?
-
-
if required_permission
-
unless user.respond_to?(required_permission) && user.send(required_permission)
-
return failure!("权限不足,需要权限: #{required_permission}")
-
end
-
end
-
-
true
-
end
-
-
# 验证必需参数
-
def validate_required_params(params, required_fields)
-
missing_fields = required_fields.select { |field| params[field].blank? }
-
-
if missing_fields.any?
-
failure!("缺少必需参数: #{missing_fields.join(', ')}")
-
return false
-
end
-
-
true
-
end
-
-
# 验证记录存在
-
def validate_record_exists(record, name = '记录')
-
unless record
-
failure!("#{name}不存在")
-
return false
-
end
-
-
unless record.persisted?
-
failure!("#{name}未保存")
-
return false
-
end
-
-
true
-
end
-
-
# 记录服务操作日志
-
def log_service_action(action, additional_info = {})
-
Rails.logger.info "Service #{service_type}: #{action} - #{additional_info}"
-
end
-
-
# 记录服务错误日志
-
def log_service_error(error, additional_info = {})
-
Rails.logger.error "Service #{service_type} Error: #{error.message}"
-
Rails.logger.error additional_info if additional_info.any?
-
end
-
end
-
# 内容导出服务
-
# 提供打卡内容的多格式导出功能
-
class ContentExportService
-
require 'prawn' # PDF生成
-
require 'prawn/table' # PDF表格
-
require 'csv' # CSV导出
-
-
class ExportOptions
-
attr_accessor :format, :check_in_ids, :user_id, :event_id, :date_from, :date_to,
-
:include_metadata, :include_comments, :include_flowers,
-
:sort_by, :sort_direction, :template
-
-
def initialize(params = {})
-
@format = params[:format] || 'pdf'
-
@check_in_ids = params[:check_in_ids]&.split(',')&.map(&:to_i)
-
@user_id = params[:user_id]&.to_i
-
@event_id = params[:event_id]&.to_i
-
@date_from = parse_date(params[:date_from])
-
@date_to = parse_date(params[:date_to])
-
@include_metadata = params[:include_metadata] != 'false'
-
@include_comments = params[:include_comments] == 'true'
-
@include_flowers = params[:include_flowers] == 'true'
-
@sort_by = params[:sort_by] || 'created_at'
-
@sort_direction = params[:sort_direction] || 'desc'
-
@template = params[:template] || 'default'
-
end
-
-
def valid_format?
-
%w[pdf markdown html txt csv].include?(format)
-
end
-
-
private
-
-
def parse_date(date_string)
-
return nil if date_string.blank?
-
Date.parse(date_string)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
-
class ExportResult
-
attr_accessor :content, :filename, :content_type, :size, :check_ins_count
-
-
def initialize
-
@content = ''
-
@filename = ''
-
@content_type = 'application/octet-stream'
-
@size = 0
-
@check_ins_count = 0
-
end
-
-
def success?
-
!content.empty?
-
end
-
end
-
-
class << self
-
# 主要导出方法
-
def export(params = {})
-
options = ExportOptions.new(params)
-
-
unless options.valid_format?
-
result = ExportResult.new
-
result.filename = "export_error.txt"
-
result.content = "不支持的导出格式: #{options.format}"
-
result.content_type = "text/plain"
-
return result
-
end
-
-
# 获取要导出的打卡记录
-
check_ins = get_check_ins_for_export(options)
-
-
if check_ins.empty?
-
result = ExportResult.new
-
result.filename = "empty_export.#{options.format}"
-
result.content = "没有找到符合条件的打卡记录"
-
result.content_type = "text/plain"
-
return result
-
end
-
-
# 根据格式执行导出
-
result = case options.format
-
when 'pdf'
-
export_to_pdf(check_ins, options)
-
when 'markdown'
-
export_to_markdown(check_ins, options)
-
when 'html'
-
export_to_html(check_ins, options)
-
when 'txt'
-
export_to_text(check_ins, options)
-
when 'csv'
-
export_to_csv(check_ins, options)
-
else
-
export_to_text(check_ins, options)
-
end
-
-
result.check_ins_count = check_ins.count
-
result
-
rescue => e
-
result = ExportResult.new
-
result.filename = "export_error.txt"
-
result.content = "导出过程中发生错误: #{e.message}"
-
result.content_type = "text/plain"
-
result
-
end
-
-
# 批量导出
-
def batch_export(params_array = [])
-
results = []
-
-
params_array.each_with_index do |params, index|
-
result = export(params)
-
result.filename = "batch_export_#{index + 1}_#{result.filename}"
-
results << result
-
end
-
-
results
-
end
-
-
# 获取导出统计信息
-
def export_statistics(params = {})
-
options = ExportOptions.new(params)
-
check_ins = get_check_ins_for_export(options)
-
-
{
-
total_check_ins: check_ins.count,
-
total_words: check_ins.sum(:word_count),
-
date_range: {
-
from: check_ins.minimum(:created_at)&.to_date,
-
to: check_ins.maximum(:created_at)&.to_date
-
},
-
users_count: check_ins.distinct.count(:user_id),
-
events_count: check_ins.joins(:reading_event).distinct.count(:reading_event_id),
-
format_options: %w[pdf markdown html txt csv]
-
}
-
end
-
-
private
-
-
# 获取要导出的打卡记录
-
def get_check_ins_for_export(options)
-
query = CheckIn.includes(:user, :reading_schedule, :reading_event, :flowers, :comments)
-
-
# 按ID筛选
-
if options.check_in_ids.present?
-
query = query.where(id: options.check_in_ids)
-
end
-
-
# 按用户筛选
-
if options.user_id.present?
-
query = query.where(user_id: options.user_id)
-
end
-
-
# 按活动筛选
-
if options.event_id.present?
-
query = query.joins(:reading_schedule).where(reading_schedules: { reading_event_id: options.event_id })
-
end
-
-
# 按日期范围筛选
-
if options.date_from.present?
-
query = query.where('check_ins.created_at >= ?', options.date_from.beginning_of_day)
-
end
-
-
if options.date_to.present?
-
query = query.where('check_ins.created_at <= ?', options.date_to.end_of_day)
-
end
-
-
# 排序
-
case options.sort_by
-
when 'created_at'
-
query = query.order("created_at #{options.sort_direction.upcase}")
-
when 'word_count'
-
query = query.order("word_count #{options.sort_direction.upcase}")
-
when 'flowers_count'
-
query = query.left_joins(:flowers).group('check_ins.id').order("COUNT(flowers.id) #{options.sort_direction.upcase}")
-
else
-
query = order(created_at: :desc)
-
end
-
-
query
-
end
-
-
# 导出为PDF
-
def export_to_pdf(check_ins, options)
-
result = ExportResult.new
-
-
# 创建PDF文档
-
Prawn::Document.generate(StringIO.new) do |pdf|
-
# 设置字体
-
pdf.font_families.update(
-
'NotoSansCJK' => {
-
normal: Rails.root.join('app', 'assets', 'fonts', 'NotoSansCJK-Regular.ttc'),
-
bold: Rails.root.join('app', 'assets', 'fonts', 'NotoSansCJK-Bold.ttc')
-
}
-
)
-
pdf.font 'NotoSansCJK'
-
-
# 标题
-
pdf.text '打卡内容导出', size: 24, style: :bold, align: :center
-
pdf.move_down 20
-
-
# 导出信息
-
if options.include_metadata
-
pdf.text "导出时间: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}", size: 10
-
pdf.text "打卡数量: #{check_ins.count}", size: 10
-
pdf.text "总字数: #{check_ins.sum(:word_count)}", size: 10
-
pdf.move_down 20
-
end
-
-
# 打卡内容
-
check_ins.each_with_index do |check_in, index|
-
pdf.start_new_page if index > 0
-
-
# 打卡标题
-
pdf.text "打卡 ##{index + 1}", size: 16, style: :bold
-
pdf.text "用户: #{check_in.user.nickname}", size: 12
-
pdf.text "时间: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}", size: 12
-
pdf.text "字数: #{check_in.word_count}", size: 12
-
pdf.text "状态: #{check_in.status_text}", size: 12
-
pdf.move_down 10
-
-
# 打卡内容
-
pdf.text "内容:", size: 14, style: :bold
-
pdf.text check_in.content, size: 12
-
pdf.move_down 10
-
-
# 小红花
-
if options.include_flowers && check_in.flowers.any?
-
pdf.text "小红花:", size: 14, style: :bold
-
check_in.flowers.each do |flower|
-
pdf.text "- #{flower.giver.nickname}: #{flower.comment}", size: 10
-
end
-
pdf.move_down 10
-
end
-
-
# 评论
-
if options.include_comments && check_in.comments.any?
-
pdf.text "评论:", size: 14, style: :bold
-
check_in.comments.each do |comment|
-
pdf.text "- #{comment.user.nickname}: #{comment.content}", size: 10
-
end
-
end
-
-
pdf.move_down 20
-
end
-
end.string
-
-
result.content = pdf_content
-
result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.pdf"
-
result.content_type = 'application/pdf'
-
result
-
end
-
-
# 导出为Markdown
-
def export_to_markdown(check_ins, options)
-
result = ExportResult.new
-
content = []
-
-
# Markdown头部
-
content << "# 打卡内容导出"
-
content << ""
-
content << "**导出时间**: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}"
-
content << "**打卡数量**: #{check_ins.count}"
-
content << "**总字数**: #{check_ins.sum(:word_count)}"
-
content << ""
-
-
# 打卡内容
-
check_ins.each_with_index do |check_in, index|
-
content << "## 打卡 ##{index + 1}"
-
content << ""
-
content << "**用户**: #{check_in.user.nickname}"
-
content << "**时间**: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}"
-
content << "**字数**: #{check_in.word_count}"
-
content << "**状态**: #{check_in.status_text}"
-
content << ""
-
-
content << "### 内容"
-
content << ""
-
content << check_in.content
-
content << ""
-
-
# 小红花
-
if options.include_flowers && check_in.flowers.any?
-
content << "### 小红花"
-
content << ""
-
check_in.flowers.each do |flower|
-
content << "- **#{flower.giver.nickname}**: #{flower.comment}"
-
end
-
content << ""
-
end
-
-
# 评论
-
if options.include_comments && check_in.comments.any?
-
content << "### 评论"
-
content << ""
-
check_in.comments.each do |comment|
-
content << "- **#{comment.user.nickname}**: #{comment.content}"
-
end
-
content << ""
-
end
-
-
content << "---"
-
content << ""
-
end
-
-
result.content = content.join("\n")
-
result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.md"
-
result.content_type = 'text/markdown'
-
result
-
end
-
-
# 导出为HTML
-
def export_to_html(check_ins, options)
-
result = ExportResult.new
-
-
html = <<~HTML
-
<!DOCTYPE html>
-
<html lang="zh-CN">
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>打卡内容导出</title>
-
<style>
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans CJK SC', sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
-
.header { border-bottom: 2px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
-
.check-in { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
-
.check-in-header { border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 15px; }
-
.user-info { color: #666; font-size: 14px; margin-bottom: 5px; }
-
.content { margin: 15px 0; }
-
.flowers, .comments { margin-top: 15px; padding: 10px; background: #f9f9f9; border-radius: 4px; }
-
.flower-item, .comment-item { margin: 5px 0; font-size: 14px; }
-
</style>
-
</head>
-
<body>
-
<div class="header">
-
<h1>打卡内容导出</h1>
-
<p><strong>导出时间</strong>: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}</p>
-
<p><strong>打卡数量</strong>: #{check_ins.count}</p>
-
<p><strong>总字数</strong>: #{check_ins.sum(:word_count)}</p>
-
</div>
-
-
<div class="check-ins">
-
HTML
-
-
check_ins.each_with_index do |check_in, index|
-
html += <<~HTML
-
<div class="check-in">
-
<div class="check-in-header">
-
<h2>打卡 ##{index + 1}</h2>
-
<div class="user-info">
-
<span><strong>用户</strong>: #{check_in.user.nickname}</span> |
-
<span><strong>时间</strong>: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}</span> |
-
<span><strong>字数</strong>: #{check_in.word_count}</span> |
-
<span><strong>状态</strong>: #{check_in.status_text}</span>
-
</div>
-
</div>
-
<div class="content">
-
<h3>内容</h3>
-
<div>#{check_in.content.gsub("\n", "<br>")}</div>
-
</div>
-
HTML
-
-
# 小红花
-
if options.include_flowers && check_in.flowers.any?
-
html += <<~HTML
-
<div class="flowers">
-
<h4>小红花</h4>
-
#{check_in.flowers.map { |flower| "<div class=\"flower-item\"><strong>#{flower.giver.nickname}</strong>: #{flower.comment}</div>" }.join}
-
</div>
-
HTML
-
end
-
-
# 评论
-
if options.include_comments && check_in.comments.any?
-
html += <<~HTML
-
<div class="comments">
-
<h4>评论</h4>
-
#{check_in.comments.map { |comment| "<div class=\"comment-item\"><strong>#{comment.user.nickname}</strong>: #{comment.content}</div>" }.join}
-
</div>
-
HTML
-
end
-
-
html += "</div>"
-
end
-
-
html += <<~HTML
-
</div>
-
</body>
-
</html>
-
HTML
-
-
result.content = html
-
result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.html"
-
result.content_type = 'text/html'
-
result
-
end
-
-
# 导出为纯文本
-
def export_to_text(check_ins, options)
-
result = ExportResult.new
-
content = []
-
-
# 文本头部
-
content << "=" * 60
-
content << "打卡内容导出"
-
content << "=" * 60
-
content << ""
-
content << "导出时间: #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}"
-
content << "打卡数量: #{check_ins.count}"
-
content << "总字数: #{check_ins.sum(:word_count)}"
-
content << ""
-
-
# 打卡内容
-
check_ins.each_with_index do |check_in, index|
-
content << "-" * 40
-
content << "打卡 ##{index + 1}"
-
content << "-" * 40
-
content << "用户: #{check_in.user.nickname}"
-
content << "时间: #{check_in.created_at.strftime('%Y-%m-%d %H:%M')}"
-
content << "字数: #{check_in.word_count}"
-
content << "状态: #{check_in.status_text}"
-
content << ""
-
content << "内容:"
-
content << check_in.content
-
content << ""
-
-
# 小红花
-
if options.include_flowers && check_in.flowers.any?
-
content << "小红花:"
-
check_in.flowers.each do |flower|
-
content << "- #{flower.giver.nickname}: #{flower.comment}"
-
end
-
content << ""
-
end
-
-
# 评论
-
if options.include_comments && check_in.comments.any?
-
content << "评论:"
-
check_in.comments.each do |comment|
-
content << "- #{comment.user.nickname}: #{comment.content}"
-
end
-
content << ""
-
end
-
-
content << ""
-
end
-
-
result.content = content.join("\n")
-
result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.txt"
-
result.content_type = 'text/plain'
-
result
-
end
-
-
# 导出为CSV
-
def export_to_csv(check_ins, options)
-
result = ExportResult.new
-
-
CSV.generate(headers: true, write_headers: true) do |csv|
-
headers = ['ID', '用户', '时间', '字数', '状态', '内容']
-
headers += ['小红花数量'] if options.include_flowers
-
headers += ['评论数量'] if options.include_comments
-
-
csv << headers
-
-
check_ins.each do |check_in|
-
row = [
-
check_in.id,
-
check_in.user.nickname,
-
check_in.created_at.strftime('%Y-%m-%d %H:%M:%S'),
-
check_in.word_count,
-
check_in.status_text,
-
check_in.content.gsub("\n", " ")
-
]
-
-
if options.include_flowers
-
row << check_in.flowers.count
-
end
-
-
if options.include_comments
-
row << check_in.comments.count
-
end
-
-
csv << row
-
end
-
end
-
-
result.content = csv_content
-
result.filename = "check_ins_export_#{Time.current.strftime('%Y%m%d_%H%M%S')}.csv"
-
result.content_type = 'text/csv'
-
result
-
end
-
end
-
end
-
-
# 扩展CheckIn模型以支持导出
-
class CheckIn
-
def status_text
-
case status
-
when 'normal'
-
'正常打卡'
-
when 'supplement'
-
'补卡'
-
when 'late'
-
'迟到'
-
else
-
status.to_s
-
end
-
end
-
end
-
# 内容格式化服务
-
# 负责处理打卡内容的格式化、分段、表情转换等
-
class ContentFormatterService
-
include ActionView::Helpers::TextHelper
-
include ActionView::Helpers::SanitizeHelper
-
-
# 表情符号映射
-
EMOJI_MAPPING = {
-
'开心' => '😊',
-
'快乐' => '😄',
-
'哈哈' => '😂',
-
'喜欢' => '❤️',
-
'爱' => '💕',
-
'赞' => '👍',
-
'加油' => '💪',
-
'思考' => '🤔',
-
'学习' => '📚',
-
'阅读' => '📖',
-
'进步' => '📈',
-
'努力' => '🌟',
-
'感谢' => '🙏',
-
'棒' => '👏',
-
'好' => '👌',
-
'支持' => '💯',
-
'鼓励' => '🎉',
-
'收获' => '🌱',
-
'成长' => '🌿'
-
}.freeze
-
-
# 敏感词列表(简化版)
-
SENSITIVE_WORDS = %w[
-
违法 暴力 色情 赌博 毒品
-
# 实际应用中应该使用更完整的敏感词库
-
].freeze
-
-
class << self
-
# 格式化内容主体方法
-
def format(content, options = {})
-
formatted_content = content.dup
-
-
# 应用各种格式化处理
-
formatted_content = sanitize_content(formatted_content)
-
formatted_content = convert_emojis(formatted_content)
-
formatted_content = format_paragraphs(formatted_content)
-
formatted_content = highlight_keywords(formatted_content, options[:keywords]) if options[:keywords].present?
-
formatted_content = add_hashtag_links(formatted_content) if options[:enable_hashtags]
-
formatted_content = truncate_content(formatted_content, options[:length]) if options[:length].present?
-
-
formatted_content
-
end
-
-
# 生成内容摘要
-
def generate_summary(content, max_length = 200)
-
# 清理内容并生成摘要
-
cleaned = sanitize_content(content)
-
cleaned = remove_formatting(cleaned)
-
-
if cleaned.length > max_length
-
# 尝试在句号或换行符处截断
-
truncated = cleaned.truncate(max_length, separator: /[,,.。!!??\n]/)
-
truncated += "..." unless truncated.end_with?('.')
-
truncated
-
else
-
cleaned
-
end
-
end
-
-
# 提取关键词
-
def extract_keywords(content, max_keywords = 5)
-
cleaned = sanitize_content(content)
-
-
# 简单的关键词提取逻辑(实际应用中可以使用更复杂的NLP算法)
-
words = cleaned.scan(/[\u4e00-\u9fa5]+|[a-zA-Z]+/)
-
.reject { |word| word.length < 2 }
-
.group_by(&:itself)
-
.transform_values(&:count)
-
.sort_by { |_, count| -count }
-
.first(max_keywords)
-
.map(&:first)
-
-
words
-
end
-
-
# 计算内容质量分数
-
def calculate_quality_score(content)
-
score = 0
-
-
# 基础分数(长度要求)
-
length = content.length
-
if length >= 50
-
score += 10
-
elsif length >= 100
-
score += 20
-
elsif length >= 200
-
score += 30
-
end
-
-
# 段落结构分数
-
paragraphs = content.split(/\n\n+/).length
-
score += [paragraphs * 2, 10].min
-
-
# 关键词多样性分数
-
keywords = extract_keywords(content, 10).length
-
score += keywords * 2
-
-
# 表情符号使用分数
-
emoji_count = content.scan(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/).length
-
score += [emoji_count, 5].min
-
-
# 敏感词检测扣分
-
sensitive_count = count_sensitive_words(content)
-
score -= sensitive_count * 10
-
-
[score, 0].max # 确保分数不为负
-
end
-
-
# 检查内容合规性
-
def check_compliance(content)
-
issues = []
-
-
# 检查敏感词
-
sensitive_words = find_sensitive_words(content)
-
if sensitive_words.any?
-
issues << {
-
type: 'sensitive_words',
-
message: "内容包含敏感词:#{sensitive_words.join(', ')}",
-
severity: 'high'
-
}
-
end
-
-
# 检查长度
-
if content.length < 50
-
issues << {
-
type: 'too_short',
-
message: "内容太短,至少需要50个字",
-
severity: 'medium'
-
}
-
end
-
-
# 检查是否为重复内容
-
if is_duplicate_content?(content)
-
issues << {
-
type: 'duplicate',
-
message: "内容疑似重复",
-
severity: 'low'
-
}
-
end
-
-
# 检查格式
-
if content.match?(/^[^\n]*$/) # 没有换行
-
issues << {
-
type: 'poor_formatting',
-
message: "建议分段以提高可读性",
-
severity: 'low'
-
}
-
end
-
-
{
-
compliant: issues.empty?,
-
issues: issues,
-
score: calculate_quality_score(content)
-
}
-
end
-
-
private
-
-
# 清理内容,移除不安全的HTML
-
def sanitize_content(content)
-
# 简单的HTML清理实现
-
cleaned = content.dup
-
cleaned.gsub!(/<script[^>]*>.*?<\/script>/mi, '')
-
cleaned.gsub!(/<style[^>]*>.*?<\/style>/mi, '')
-
cleaned.gsub!(/<[^>]*>/, '')
-
cleaned.strip
-
end
-
-
# 转换表情符号
-
def convert_emojis(content)
-
formatted = content.dup
-
-
EMOJI_MAPPING.each do |text, emoji|
-
formatted.gsub!(/#{text}/i, emoji)
-
end
-
-
formatted
-
end
-
-
# 格式化段落
-
def format_paragraphs(content)
-
# 将连续的换行符转换为段落
-
paragraphs = content.split(/\n\n+/)
-
-
formatted_paragraphs = paragraphs.map do |paragraph|
-
# 处理单个段落内的换行
-
lines = paragraph.split(/\n/)
-
-
if lines.length == 1
-
# 单行内容
-
"<p>#{lines.first.strip}</p>"
-
else
-
# 多行内容,使用<br>连接
-
"<p>#{lines.map(&:strip).join('<br>')}</p>"
-
end
-
end
-
-
formatted_paragraphs.join("\n")
-
end
-
-
# 高亮关键词
-
def highlight_keywords(content, keywords)
-
formatted = content.dup
-
-
Array(keywords).each do |keyword|
-
next if keyword.blank?
-
formatted.gsub!(/(#{Regexp.escape(keyword)})/i, '<mark>\1</mark>')
-
end
-
-
formatted
-
end
-
-
# 添加话题标签链接
-
def add_hashtag_links(content)
-
content.gsub(/#([^#\s]+)#?/) do |match|
-
hashtag = $1
-
"<a href='/search?q=%23#{hashtag}' class='hashtag'>##{hashtag}</a>"
-
end
-
end
-
-
# 截断内容
-
def truncate_content(content, length)
-
# 简单的截断实现
-
if content.length > length
-
last_space = content.rindex(' ', length - 3)
-
if last_space && last_space > 0
-
content[0, last_space] + "..."
-
else
-
content[0, length - 3] + "..."
-
end
-
else
-
content
-
end
-
end
-
-
# 移除格式化标签
-
def remove_formatting(content)
-
# 移除所有HTML标签
-
content.gsub(/<[^>]*>/, '').strip
-
end
-
-
# 统计敏感词数量
-
def count_sensitive_words(content)
-
count = 0
-
SENSITIVE_WORDS.each do |word|
-
count += content.scan(/#{word}/i).length
-
end
-
count
-
end
-
-
# 查找敏感词
-
def find_sensitive_words(content)
-
found_words = []
-
-
SENSITIVE_WORDS.each do |word|
-
if content.match?(/#{word}/i)
-
found_words << word
-
end
-
end
-
-
found_words
-
end
-
-
# 检查是否为重复内容(简化版)
-
def is_duplicate_content?(content)
-
# 这里可以实现更复杂的重复内容检测算法
-
# 比如计算文本指纹、与历史记录对比等
-
-
# 简单的重复检测:检查是否有大量重复字符
-
max_consecutive_chars = content.scan(/(.)\1{5,}/).length
-
return true if max_consecutive_chars > 0
-
-
# 检查是否大部分内容都是标点符号
-
punctuation_ratio = content.count('.,!?;:,。!?;:').to_f / content.length
-
return true if punctuation_ratio > 0.3
-
-
false
-
end
-
-
# 检查是否需要举报
-
def should_report_content?(content, check_in = nil)
-
compliance = check_compliance(content)
-
-
# 包含敏感词的建议自动举报
-
if compliance[:issues].any? { |issue| issue[:type] == 'sensitive_words' }
-
return {
-
should_report: true,
-
reason: :sensitive_words,
-
auto_report: true,
-
detected_words: compliance[:issues].find { |i| i[:type] == 'sensitive_words' }&.dig(:detected_words) || []
-
}
-
end
-
-
# 质量分数过低的建议举报
-
if compliance[:score] < 20
-
return {
-
should_report: true,
-
reason: :inappropriate_content,
-
auto_report: false,
-
quality_score: compliance[:score]
-
}
-
end
-
-
{ should_report: false }
-
end
-
-
# 生成举报建议
-
def generate_report_suggestion(content, check_in = nil)
-
analysis = should_report_content?(content, check_in)
-
-
if analysis[:should_report]
-
suggestion = case analysis[:reason]
-
when :sensitive_words
-
{
-
reason: :sensitive_words,
-
message: "内容包含敏感词:#{analysis[:detected_words].join(', ')}",
-
auto_report: analysis[:auto_report],
-
priority: 'high'
-
}
-
when :inappropriate_content
-
{
-
reason: :inappropriate_content,
-
message: "内容质量过低,可能包含不当内容",
-
auto_report: false,
-
priority: 'medium'
-
}
-
else
-
{
-
reason: :other,
-
message: "内容可能需要人工审核",
-
auto_report: false,
-
priority: 'low'
-
}
-
end
-
else
-
suggestion = {
-
reason: nil,
-
message: "内容正常,无需举报",
-
auto_report: false,
-
priority: 'low'
-
}
-
end
-
-
suggestion.merge(
-
compliance: check_compliance(content),
-
sensitive_words: find_sensitive_words(content),
-
quality_score: calculate_quality_score(content)
-
)
-
end
-
-
# 检查用户举报权限
-
def can_report_content?(user, check_in)
-
# 不能举报自己的内容
-
return false if user == check_in.user
-
-
# 检查是否已经举报过
-
existing_report = ContentReport.find_by(user: user, check_in: check_in)
-
return false if existing_report
-
-
true
-
end
-
-
# 预处理举报内容
-
def preprocess_report_content(content)
-
# 清理和预处理举报内容
-
sanitized = sanitize_content(content)
-
# 简单的截断实现
-
if sanitized.length > 1000
-
truncated = sanitized[0, 997] + "..."
-
else
-
truncated = sanitized
-
end
-
truncated.strip
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ContentModerationAnalyticsService - 内容审核统计分析服务
-
# 专门负责举报数据的统计、分析和报告生成
-
class ContentModerationAnalyticsService < ApplicationService
-
include ServiceInterface
-
attr_reader :start_date, :end_date, :options
-
-
def initialize(start_date: nil, end_date: nil, options: {})
-
super()
-
@start_date = start_date || 30.days.ago.to_date
-
@end_date = end_date || Date.current
-
@options = options.with_indifferent_access
-
end
-
-
# 获取综合统计报告
-
def call
-
handle_errors do
-
validate_date_params
-
generate_comprehensive_report
-
end
-
self
-
end
-
-
# 获取基本统计数据
-
def self.get_basic_statistics(days = 30)
-
new(
-
start_date: days.days.ago.to_date,
-
end_date: Date.current
-
).basic_statistics
-
end
-
-
# 生成审核报告
-
def self.generate_moderation_report(start_date = nil, end_date = nil)
-
new(
-
start_date: start_date,
-
end_date: end_date
-
).generate_moderation_report
-
end
-
-
private
-
-
# 验证日期参数
-
def validate_date_params
-
return failure!("开始日期不能为空") unless start_date
-
return failure!("结束日期不能为空") unless end_date
-
return failure!("开始日期不能晚于结束日期") if start_date > end_date
-
return failure!("时间范围不能超过一年") if (end_date - start_date).days > 365
-
-
true
-
end
-
-
# 生成综合报告
-
def generate_comprehensive_report
-
reports = find_reports_in_period
-
-
success!({
-
period: {
-
start: start_date,
-
end: end_date,
-
days_count: (end_date - start_date).to_i + 1
-
},
-
summary: generate_summary_statistics(reports),
-
trends: generate_trend_analysis(reports),
-
breakdown: generate_breakdown_analysis(reports),
-
efficiency: generate_efficiency_metrics(reports),
-
recommendations: generate_recommendations(reports)
-
})
-
end
-
-
# 查找时间范围内的举报
-
def find_reports_in_period
-
ContentReport.where(
-
created_at: start_date.beginning_of_day..end_date.end_of_day
-
).includes(:user, :admin, :target_content)
-
end
-
-
# 生成摘要统计
-
def generate_summary_statistics(reports)
-
{
-
total_reports: reports.count,
-
pending_reports: reports.pending.count,
-
processed_reports: reports.where.not(status: :pending).count,
-
auto_processed_reports: reports.where.not(admin_id: nil).where('reports.created_at = reports.updated_at').count,
-
average_processing_time: calculate_average_processing_time(reports),
-
reports_per_day: (reports.count.to_f / ((end_date - start_date).to_i + 1)).round(2)
-
}
-
end
-
-
# 生成趋势分析
-
def generate_trend_analysis(reports)
-
{
-
daily_trends: reports.group('DATE(created_at)').count,
-
weekly_trends: generate_weekly_trends(reports),
-
monthly_trends: generate_monthly_trends(reports),
-
peak_hours: identify_peak_hours(reports),
-
growth_rate: calculate_growth_rate(reports)
-
}
-
end
-
-
# 生成周趋势
-
def generate_weekly_trends(reports)
-
reports.group("strftime('%Y-%W', created_at)").count
-
end
-
-
# 生成月趋势
-
def generate_monthly_trends(reports)
-
reports.group("strftime('%Y-%m', created_at)").count
-
end
-
-
# 识别举报高峰时段
-
def identify_peak_hours(reports)
-
reports.group("strftime('%H', created_at)").count.sort_by { |_, count| -count }.first(5)
-
end
-
-
# 计算增长率
-
def calculate_growth_rate(reports)
-
return {} if reports.count < 2
-
-
first_half = reports.where(created_at: start_date..(start_date + ((end_date - start_date) / 2)))
-
second_half = reports.where(created_at: ((start_date + ((end_date - start_date) / 2) + 1.day))..end_date)
-
-
{
-
first_half_count: first_half.count,
-
second_half_count: second_half.count,
-
growth_rate: calculate_percentage_change(first_half.count, second_half.count)
-
}
-
end
-
-
# 生成分类分析
-
def generate_breakdown_analysis(reports)
-
{
-
by_reason: reports.group(:reason).count,
-
by_status: reports.group(:status).count,
-
by_content_type: reports.joins(:target_content).group('target_contents.type').count,
-
by_admin: reports.joins(:admin).where.not(admin_id: nil).group('users.nickname').count,
-
by_reporter: reports.joins(:user).group('users.nickname').count.order('count DESC').limit(10),
-
by_action_taken: reports.joins(:target_content).where(target_contents: { hidden: true }).count
-
}
-
end
-
-
# 生成效率指标
-
def generate_efficiency_metrics(reports)
-
processed_reports = reports.where.not(status: :pending)
-
-
{
-
processing_rate: calculate_processing_rate(reports),
-
average_resolution_time: calculate_average_processing_time(processed_reports),
-
auto_processing_rate: calculate_auto_processing_rate(reports),
-
admin_workload: calculate_admin_workload(reports),
-
repeat_content_reports: calculate_repeat_content_reports(reports)
-
}
-
end
-
-
# 计算处理率
-
def calculate_processing_rate(reports)
-
return 0 if reports.count == 0
-
processed = reports.where.not(status: :pending).count
-
(processed.to_f / reports.count * 100).round(2)
-
end
-
-
# 计算平均处理时间
-
def calculate_average_processing_time(reports)
-
return 0 if reports.empty?
-
-
processed_reports = reports.where.not(status: :pending).where.not(updated_at: nil)
-
return 0 if processed_reports.empty?
-
-
total_time = processed_reports.sum do |report|
-
(report.updated_at - report.created_at) / 1.hour # 转换为小时
-
end
-
-
(total_time / processed_reports.count).round(2)
-
end
-
-
# 计算自动处理率
-
def calculate_auto_processing_rate(reports)
-
return 0 if reports.count == 0
-
-
auto_processed = reports.where.not(admin_id: nil)
-
.where('reports.created_at = reports.updated_at')
-
.count
-
-
(auto_processed.to_f / reports.count * 100).round(2)
-
end
-
-
# 计算管理员工作量
-
def calculate_admin_workload(reports)
-
reports.joins(:admin)
-
.where.not(admin_id: nil)
-
.group('users.nickname')
-
.count
-
end
-
-
# 计算重复内容举报
-
def calculate_repeat_content_reports(reports)
-
content_counts = reports.group(:target_content_id).count
-
repeated_contents = content_counts.select { |_, count| count > 1 }
-
-
{
-
total_repeated_contents: repeated_contents.count,
-
average_reports_per_content: repeated_contents.empty? ? 0 :
-
(repeated_contents.values.sum.to_f / repeated_contents.count).round(2),
-
most_reported_content: repeated_contents.max_by { |_, count| count }
-
}
-
end
-
-
# 生成建议
-
def generate_recommendations(reports)
-
recommendations = []
-
-
# 分析处理效率
-
processing_rate = calculate_processing_rate(reports)
-
if processing_rate < 80
-
recommendations << {
-
type: 'efficiency',
-
priority: 'high',
-
title: '提高举报处理效率',
-
description: "当前处理率为#{processing_rate}%,建议优化审核流程或增加审核人员"
-
}
-
end
-
-
# 分析自动处理效果
-
auto_rate = calculate_auto_processing_rate(reports)
-
if auto_rate < 30
-
recommendations << {
-
type: 'automation',
-
priority: 'medium',
-
title: '增加自动化处理',
-
description: "当前自动处理率为#{auto_rate}%,建议增加敏感词检测等自动化规则"
-
}
-
end
-
-
# 分析举报类型分布
-
reason_breakdown = reports.group(:reason).count
-
if reason_breakdown['sensitive_words']&.to_i&.>(reason_breakdown.values.sum * 0.4)
-
recommendations << {
-
type: 'prevention',
-
priority: 'high',
-
title: '加强敏感词预防',
-
description: '敏感词举报占比较高,建议在内容发布时进行更好的预检查'
-
}
-
end
-
-
recommendations
-
end
-
-
# 基本统计数据
-
def basic_statistics
-
reports = find_reports_in_period
-
-
{
-
total_reports: reports.count,
-
pending_reports: reports.pending.count,
-
processed_reports: reports.where.not(status: :pending).count,
-
by_reason: reports.group(:reason).count,
-
by_status: reports.group(:status).count
-
}
-
end
-
-
# 生成审核报告
-
def generate_moderation_report
-
reports = find_reports_in_period
-
-
{
-
period: { start: start_date, end: end_date },
-
summary: {
-
total_reports: reports.count,
-
pending_reports: reports.pending.count,
-
processed_reports: reports.where.not(status: :pending).count,
-
auto_processed_reports: reports.where.not(admin_id: nil).count
-
},
-
by_reason: reports.group(:reason).count,
-
by_status: reports.group(:status).count,
-
by_admin: reports.joins(:admin).group('users.nickname').count,
-
daily_trends: reports.group('DATE(created_at)').count
-
}
-
end
-
-
# 计算百分比变化
-
def calculate_percentage_change(old_value, new_value)
-
return 0 if old_value == 0
-
(((new_value - old_value).to_f / old_value) * 100).round(2)
-
end
-
end
-
# frozen_string_literal: true
-
-
# ContentModerationQueryService - 内容审核查询服务
-
# 专门负责举报相关数据的查询和检索
-
class ContentModerationQueryService < ApplicationService
-
include ServiceInterface
-
attr_reader :current_user, :filters, :pagination_options
-
-
def initialize(current_user: nil, filters: {}, pagination_options: {})
-
super()
-
@current_user = current_user
-
@filters = filters.with_indifferent_access
-
@pagination_options = pagination_options.with_indifferent_access
-
end
-
-
# 获取待处理的举报
-
def call
-
handle_errors do
-
validate_query_permissions
-
apply_filters_and_paginate
-
format_query_results
-
end
-
self
-
end
-
-
# 类方法:获取待处理的举报
-
def self.get_pending_reports(limit: 50, current_user: nil)
-
new(
-
current_user: current_user,
-
filters: { status: 'pending' },
-
pagination_options: { limit: limit }
-
).call
-
end
-
-
# 类方法:获取高优先级举报
-
def self.get_high_priority_reports(current_user: nil)
-
new(
-
current_user: current_user,
-
filters: { priority: 'high' }
-
).call
-
end
-
-
# 类方法:获取用户举报历史
-
def self.get_user_report_history(user, limit: 20, current_user: nil)
-
new(
-
current_user: current_user,
-
filters: { user_id: user.id },
-
pagination_options: { limit: limit }
-
).call
-
end
-
-
# 类方法:获取被举报的内容
-
def self.get_reported_content(limit: 50, status: nil, current_user: nil)
-
query_filters = { limit: limit }
-
query_filters[:status] = status if status.present?
-
-
new(
-
current_user: current_user,
-
filters: query_filters
-
).get_reported_content_data
-
end
-
-
# 类方法:搜索举报
-
def self.search_reports(query, current_user: nil)
-
new(
-
current_user: current_user,
-
filters: { search: query }
-
).call
-
end
-
-
private
-
-
# 验证查询权限
-
def validate_query_permissions
-
# 检查用户是否有权限查看举报数据
-
if current_user && !current_user.can_approve_events? && !user_querying_own_reports?
-
failure!("无权限查看举报数据")
-
return false
-
end
-
-
true
-
end
-
-
# 检查是否查询自己的举报
-
def user_querying_own_reports?
-
filters[:user_id] == current_user&.id
-
end
-
-
# 应用过滤器和分页
-
def apply_filters_and_paginate
-
@reports = base_query
-
-
# 应用各种过滤器
-
apply_status_filter
-
apply_reason_filter
-
apply_user_filter
-
apply_admin_filter
-
apply_date_filter
-
apply_priority_filter
-
apply_search_filter
-
-
# 应用排序
-
apply_ordering
-
-
# 应用分页
-
apply_pagination
-
-
true
-
end
-
-
# 基础查询
-
def base_query
-
ContentReport.includes(:user, :admin, :target_content)
-
end
-
-
# 应用状态过滤器
-
def apply_status_filter
-
return unless filters[:status].present?
-
-
status_value = filters[:status]
-
case status_value
-
when 'pending'
-
@reports = @reports.where(status: :pending)
-
when 'processed'
-
@reports = @reports.where.not(status: :pending)
-
when 'approved'
-
@reports = @reports.where(status: :approved)
-
when 'rejected'
-
@reports = @reports.where(status: :rejected)
-
else
-
@reports = @reports.where(status: status_value)
-
end
-
end
-
-
# 应用原因过滤器
-
def apply_reason_filter
-
return unless filters[:reason].present?
-
@reports = @reports.where(reason: filters[:reason])
-
end
-
-
# 应用用户过滤器
-
def apply_user_filter
-
return unless filters[:user_id].present?
-
@reports = @reports.where(user_id: filters[:user_id])
-
end
-
-
# 应用管理员过滤器
-
def apply_admin_filter
-
return unless filters[:admin_id].present?
-
@reports = @reports.where(admin_id: filters[:admin_id])
-
end
-
-
# 应用日期过滤器
-
def apply_date_filter
-
if filters[:start_date].present?
-
start_date = Date.parse(filters[:start_date])
-
@reports = @reports.where('created_at >= ?', start_date.beginning_of_day)
-
end
-
-
if filters[:end_date].present?
-
end_date = Date.parse(filters[:end_date])
-
@reports = @reports.where('created_at <= ?', end_date.end_of_day)
-
end
-
-
if filters[:days_ago].present?
-
days_ago = filters[:days_ago].to_i
-
@reports = @reports.where('created_at >= ?', days_ago.days.ago)
-
end
-
end
-
-
# 应用优先级过滤器
-
def apply_priority_filter
-
return unless filters[:priority].present?
-
-
case filters[:priority]
-
when 'high'
-
@reports = @reports.where(reason: %w[sensitive_words harassment])
-
when 'medium'
-
@reports = @reports.where(reason: %w[inappropriate_content spam])
-
when 'low'
-
@reports = @reports.where(reason: %w[other])
-
end
-
end
-
-
# 应用搜索过滤器
-
def apply_search_filter
-
return unless filters[:search].present?
-
-
search_term = "%#{filters[:search]}%"
-
@reports = @reports.joins(:user, :target_content)
-
.where(
-
'users.nickname ILIKE ? OR target_contents.content ILIKE ? OR content_reports.description ILIKE ?',
-
search_term, search_term, search_term
-
)
-
end
-
-
# 应用排序
-
def apply_ordering
-
sort_field = filters[:sort_by] || 'created_at'
-
sort_direction = filters[:sort_direction] || 'desc'
-
-
valid_fields = %w[created_at updated_at reason status user_id admin_id]
-
if valid_fields.include?(sort_field)
-
@reports = @reports.order(sort_field => sort_direction)
-
else
-
@reports = @reports.order(created_at: :desc)
-
end
-
end
-
-
# 应用分页
-
def apply_pagination
-
limit = pagination_options[:limit] || 20
-
page = pagination_options[:page] || 1
-
offset = (page.to_i - 1) * limit.to_i
-
-
@reports = @reports.limit(limit).offset(offset)
-
-
# 记录分页信息用于响应
-
@pagination_info = {
-
current_page: page.to_i,
-
per_page: limit.to_i,
-
total_count: @reports.count,
-
has_next_page: (@reports.count > (page.to_i * limit.to_i))
-
}
-
end
-
-
# 格式化查询结果
-
def format_query_results
-
reports_data = @reports.map do |report|
-
format_single_report(report)
-
end
-
-
response_data = {
-
reports: reports_data
-
}
-
-
# 添加分页信息
-
response_data[:pagination] = @pagination_info if @pagination_info
-
-
# 添加统计信息
-
response_data[:summary] = generate_query_summary if filters[:include_summary]
-
-
success!(response_data)
-
end
-
-
# 格式化单个举报数据
-
def format_single_report(report)
-
{
-
id: report.id,
-
reason: report.reason,
-
description: report.description,
-
status: report.status,
-
created_at: report.created_at,
-
updated_at: report.updated_at,
-
reporter: format_user_data(report.user),
-
admin: format_user_data(report.admin),
-
target_content: format_target_content(report.target_content),
-
auto_processed: report.auto_processed?,
-
processing_notes: report.notes
-
}
-
end
-
-
# 格式化用户数据
-
def format_user_data(user)
-
return nil unless user
-
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url,
-
role: user.role_display_name
-
}
-
end
-
-
# 格式化目标内容数据
-
def format_target_content(content)
-
return nil unless content
-
-
{
-
id: content.id,
-
type: content.class.name,
-
content_preview: content.respond_to?(:content) ? content.content.truncate(100) : '',
-
user: format_user_data(content.user),
-
created_at: content.created_at,
-
hidden: content.respond_to?(:hidden?) ? content.hidden? : false
-
}
-
end
-
-
# 生成查询摘要
-
def generate_query_summary
-
base_query = ContentReport.all
-
apply_filters_to_summary_query(base_query)
-
-
{
-
total_reports: base_query.count,
-
pending_reports: base_query.where(status: :pending).count,
-
processed_reports: base_query.where.not(status: :pending).count,
-
by_status: base_query.group(:status).count,
-
by_reason: base_query.group(:reason).count
-
}
-
end
-
-
# 对摘要查询应用过滤器
-
def apply_filters_to_summary_query(query)
-
# 这里复制上面的过滤器逻辑,但不需要分页和排序
-
if filters[:status].present?
-
query = query.where(status: filters[:status])
-
end
-
-
if filters[:reason].present?
-
query = query.where(reason: filters[:reason])
-
end
-
-
if filters[:user_id].present?
-
query = query.where(user_id: filters[:user_id])
-
end
-
-
query
-
end
-
-
# 获取被举报的内容数据
-
def get_reported_content_data
-
# 这是一个特殊查询,直接返回被举报的内容
-
query = ContentReport.joins(:target_content)
-
.includes(:target_content, :user)
-
.distinct
-
-
if filters[:status].present?
-
query = query.where(content_reports: { status: filters[:status] })
-
end
-
-
contents = query.order('content_reports.created_at DESC')
-
.limit(filters[:limit] || 50)
-
-
{
-
reported_contents: contents.map do |report|
-
{
-
content: format_target_content(report.target_content),
-
reports_count: report.target_content.content_reports.count,
-
latest_report: format_single_report(report)
-
}
-
end
-
}
-
end
-
end
-
# frozen_string_literal: true
-
-
# ContentModerationService - 内容审核服务(重构版)
-
# 作为内容审核相关服务的协调器,提供统一的接口
-
class ContentModerationService < ApplicationService
-
attr_reader :action, :params, :current_user
-
-
def initialize(action:, params: {}, current_user: nil)
-
super()
-
@action = action
-
@params = params.with_indifferent_access
-
@current_user = current_user
-
end
-
-
# 主要调用方法
-
def call
-
handle_errors do
-
validate_action
-
execute_action
-
end
-
self
-
end
-
-
# 类方法:创建举报
-
def self.create_report(user, target_content, reason:, description: nil)
-
new(
-
action: :create_report,
-
params: {
-
user: user,
-
target_content: target_content,
-
reason: reason,
-
description: description
-
}
-
).call
-
end
-
-
# 类方法:批量处理举报
-
def self.batch_process_reports(admin, report_ids, action:, notes: nil)
-
new(
-
action: :batch_process,
-
params: {
-
admin: admin,
-
report_ids: report_ids,
-
action: action,
-
notes: notes
-
}
-
).call
-
end
-
-
# 类方法:获取举报统计
-
def self.get_statistics(days = 30)
-
new(
-
action: :get_statistics,
-
params: { days: days }
-
).call
-
end
-
-
# 类方法:获取待处理的举报
-
def self.get_pending_reports(limit: 50, current_user: nil)
-
new(
-
action: :get_pending_reports,
-
params: { limit: limit },
-
current_user: current_user
-
).call
-
end
-
-
# 类方法:获取高优先级举报
-
def self.get_high_priority_reports(current_user: nil)
-
new(
-
action: :get_high_priority_reports,
-
current_user: current_user
-
).call
-
end
-
-
# 类方法:生成审核报告
-
def self.generate_moderation_report(start_date = nil, end_date = nil)
-
new(
-
action: :generate_report,
-
params: {
-
start_date: start_date,
-
end_date: end_date
-
}
-
).call
-
end
-
-
# 类方法:获取用户举报历史
-
def self.get_user_report_history(user, limit: 20, current_user: nil)
-
new(
-
action: :get_user_history,
-
params: {
-
user: user,
-
limit: limit
-
},
-
current_user: current_user
-
).call
-
end
-
-
# 类方法:获取被举报的内容
-
def self.get_reported_content(limit: 50, status: nil, current_user: nil)
-
new(
-
action: :get_reported_content,
-
params: {
-
limit: limit,
-
status: status
-
},
-
current_user: current_user
-
).call
-
end
-
-
# 类方法:检查内容是否需要自动审核
-
def self.check_content_for_review(content)
-
new(
-
action: :check_content,
-
params: { content: content }
-
).call
-
end
-
-
# 类方法:搜索举报
-
def self.search_reports(query, current_user: nil)
-
ContentModerationQueryService.search_reports(query, current_user: current_user)
-
end
-
-
private
-
-
# 验证操作
-
def validate_action
-
valid_actions = [
-
:create_report, :batch_process, :get_statistics, :get_pending_reports,
-
:get_high_priority_reports, :generate_report, :get_user_history,
-
:get_reported_content, :check_content
-
]
-
-
unless valid_actions.include?(action)
-
failure!("不支持的操作: #{action}")
-
return false
-
end
-
-
true
-
end
-
-
# 执行具体操作
-
def execute_action
-
result = case action
-
when :create_report
-
create_report_action
-
when :batch_process
-
batch_process_action
-
when :get_statistics
-
get_statistics_action
-
when :get_pending_reports
-
get_pending_reports_action
-
when :get_high_priority_reports
-
get_high_priority_reports_action
-
when :generate_report
-
generate_report_action
-
when :get_user_history
-
get_user_history_action
-
when :get_reported_content
-
get_reported_content_action
-
when :check_content
-
check_content_action
-
end
-
-
if result&.success?
-
success!(result.data)
-
else
-
failure!(result&.error_messages || ["操作失败"])
-
end
-
end
-
-
# 创建举报操作
-
def create_report_action
-
ReportCreationService.new(
-
user: params[:user],
-
target_content: params[:target_content],
-
reason: params[:reason],
-
description: params[:description]
-
).call
-
end
-
-
# 批量处理操作
-
def batch_process_action
-
ReportProcessingService.new(
-
admin: params[:admin],
-
report_ids: params[:report_ids],
-
action: params[:action],
-
notes: params[:notes]
-
).call
-
end
-
-
# 获取统计操作
-
def get_statistics_action
-
days = params[:days] || 30
-
service = ContentModerationAnalyticsService.new(
-
start_date: days.days.ago.to_date,
-
end_date: Date.current
-
)
-
service.call
-
service
-
end
-
-
# 获取待处理举报操作
-
def get_pending_reports_action
-
limit = params[:limit] || 50
-
ContentModerationQueryService.get_pending_reports(
-
limit: limit,
-
current_user: current_user
-
)
-
end
-
-
# 获取高优先级举报操作
-
def get_high_priority_reports_action
-
ContentModerationQueryService.get_high_priority_reports(
-
current_user: current_user
-
)
-
end
-
-
# 生成报告操作
-
def generate_report_action
-
ContentModerationAnalyticsService.generate_moderation_report(
-
params[:start_date],
-
params[:end_date]
-
)
-
end
-
-
# 获取用户历史操作
-
def get_user_history_action
-
limit = params[:limit] || 20
-
ContentModerationQueryService.get_user_report_history(
-
params[:user],
-
limit: limit,
-
current_user: current_user
-
)
-
end
-
-
# 获取被举报内容操作
-
def get_reported_content_action
-
ContentModerationQueryService.get_reported_content(
-
limit: params[:limit],
-
status: params[:status],
-
current_user: current_user
-
)
-
end
-
-
# 检查内容操作
-
def check_content_action
-
content = params[:content]
-
return failure!("内容不能为空") unless content
-
-
# 这里应该调用内容检查服务
-
# 为了简化,返回一个基本的检查结果
-
{
-
needs_review: false,
-
priority: 'low',
-
issues: []
-
}
-
end
-
end
-
# 内容搜索服务
-
# 提供打卡内容的全文搜索、高级搜索和推荐功能
-
class ContentSearchService
-
include ActionView::Helpers::SanitizeHelper
-
-
class SearchOptions
-
attr_accessor :query, :event_id, :user_id, :date_from, :date_to, :status,
-
:quality_min, :quality_max, :keywords, :sort_by, :sort_direction,
-
:page, :per_page
-
-
def initialize(params = {})
-
@query = params[:query]&.strip
-
@event_id = params[:event_id]
-
@user_id = params[:user_id]
-
@date_from = parse_date(params[:date_from])
-
@date_to = parse_date(params[:date_to])
-
@status = params[:status]
-
@quality_min = params[:quality_min]&.to_i
-
@quality_max = params[:quality_max]&.to_i
-
@keywords = params[:keywords]&.split(',')&.map(&:strip)
-
@sort_by = params[:sort_by] || 'relevance'
-
@sort_direction = params[:sort_direction] || 'desc'
-
@page = params[:page]&.to_i || 1
-
@per_page = params[:per_page]&.to_i || 20
-
end
-
-
private
-
-
def parse_date(date_string)
-
return nil if date_string.blank?
-
Date.parse(date_string)
-
rescue ArgumentError, TypeError
-
nil
-
end
-
end
-
-
class SearchResult
-
attr_accessor :check_ins, :total_count, :total_pages, :current_page,
-
:suggestions, :search_time, :facets
-
-
def initialize
-
@check_ins = []
-
@total_count = 0
-
@total_pages = 0
-
@current_page = 1
-
@suggestions = []
-
@search_time = 0
-
@facets = {}
-
end
-
-
def to_h
-
{
-
check_ins: check_ins.map(&:to_search_result_h),
-
pagination: {
-
current_page: current_page,
-
total_pages: total_pages,
-
total_count: total_count,
-
per_page: search_options&.per_page || 20
-
},
-
suggestions: suggestions,
-
search_time: search_time,
-
facets: facets
-
}
-
end
-
-
def search_options=(options)
-
@search_options = options
-
end
-
-
attr_reader :search_options
-
end
-
-
class << self
-
# 主要搜索方法
-
def search(params = {})
-
options = SearchOptions.new(params)
-
result = SearchResult.new
-
result.search_options = options
-
-
start_time = Time.current
-
-
# 执行搜索
-
check_ins = perform_search(options)
-
-
# 统计总数
-
total_count = count_search_results(options)
-
-
# 分页
-
paginated_check_ins = check_ins.includes(:user, :reading_schedule, :flowers)
-
.limit(options.per_page)
-
.offset((options.page - 1) * options.per_page)
-
-
# 计算搜索建议
-
suggestions = generate_suggestions(options)
-
-
# 生成搜索统计
-
facets = generate_facets(check_ins)
-
-
end_time = Time.current
-
-
result.check_ins = paginated_check_ins.to_a
-
result.total_count = total_count
-
result.total_pages = (total_count.to_f / options.per_page).ceil
-
result.current_page = options.page
-
result.suggestions = suggestions
-
result.search_time = ((end_time - start_time) * 1000).round(2)
-
result.facets = facets
-
-
result
-
end
-
-
# 高级搜索
-
def advanced_search(params = {})
-
# 高级搜索支持更复杂的条件组合
-
options = SearchOptions.new(params)
-
-
# 构建复杂查询
-
check_ins = build_advanced_query(options)
-
-
# 应用排序
-
check_ins = apply_sorting(check_ins, options)
-
-
{
-
check_ins: check_ins.includes(:user, :reading_schedule),
-
options: options
-
}
-
end
-
-
# 推荐相关内容
-
def recommend_related(check_in, limit = 5)
-
# 基于内容相似性推荐相关打卡
-
keywords = check_in.keywords(10)
-
-
related_check_ins = CheckIn.joins(:reading_schedule)
-
.where.not(id: check_in.id)
-
.where(reading_schedules: { reading_event_id: check_in.reading_event_id })
-
-
# 基于关键词匹配
-
if keywords.any?
-
keyword_conditions = keywords.map { |keyword| "check_ins.content LIKE ?" }.join(' OR ')
-
keyword_values = keywords.map { |keyword| "%#{keyword}%" }
-
-
related_check_ins = related_check_ins.where(keyword_conditions, *keyword_values)
-
end
-
-
# 按质量和时间排序
-
related_check_ins.order('created_at DESC').limit(limit)
-
end
-
-
# 热门关键词
-
def popular_keywords(limit = 20, days = 30)
-
start_date = days.days.ago.to_date
-
-
# 简化的关键词统计(实际应用中可以使用更复杂的算法)
-
recent_check_ins = CheckIn.where('created_at >= ?', start_date)
-
-
keyword_counts = Hash.new(0)
-
-
recent_check_ins.find_each do |check_in|
-
check_in.keywords(5).each do |keyword|
-
keyword_counts[keyword] += 1
-
end
-
end
-
-
keyword_counts.sort_by { |_, count| -count }.first(limit).to_h
-
end
-
-
# 搜索趋势
-
def search_trends(days = 7)
-
start_date = days.days.ago.to_date
-
-
daily_stats = CheckIn.where('created_at >= ?', start_date)
-
.group('DATE(created_at)')
-
.count
-
-
(0...days).map do |i|
-
date = (Date.today - days + 1 + i)
-
{
-
date: date,
-
count: daily_stats[date] || 0
-
}
-
end
-
end
-
-
private
-
-
# 执行基础搜索
-
def perform_search(options)
-
query = CheckIn.joins(:user, :reading_schedule)
-
-
# 文本搜索
-
if options.query.present?
-
query = apply_text_search(query, options.query)
-
end
-
-
# 活动筛选
-
if options.event_id.present?
-
query = query.where(reading_schedules: { reading_event_id: options.event_id })
-
end
-
-
# 用户筛选
-
if options.user_id.present?
-
query = query.where(user_id: options.user_id)
-
end
-
-
# 日期范围筛选
-
if options.date_from.present?
-
query = query.where('check_ins.created_at >= ?', options.date_from.beginning_of_day)
-
end
-
-
if options.date_to.present?
-
query = query.where('check_ins.created_at <= ?', options.date_to.end_of_day)
-
end
-
-
# 状态筛选
-
if options.status.present?
-
query = query.where(status: options.status)
-
end
-
-
# 质量分数筛选
-
if options.quality_min.present?
-
# 这里需要添加quality_score字段的计算逻辑
-
# 暂时使用简化版本
-
query = query.where('word_count >= ?', options.quality_min * 10)
-
end
-
-
if options.quality_max.present?
-
query = query.where('word_count <= ?', options.quality_max * 10)
-
end
-
-
# 关键词筛选
-
if options.keywords.present?
-
keyword_conditions = options.keywords.map { |keyword| "check_ins.content LIKE ?" }.join(' OR ')
-
keyword_values = options.keywords.map { |keyword| "%#{keyword}%" }
-
-
query = query.where(keyword_conditions, *keyword_values)
-
end
-
-
query
-
end
-
-
# 应用文本搜索
-
def apply_text_search(query, search_query)
-
# 简单的全文搜索实现
-
# 实际应用中可以使用PostgreSQL的全文搜索或Elasticsearch
-
-
search_terms = search_query.split(/\s+/).reject(&:blank?)
-
-
search_terms.each do |term|
-
query = query.where('check_ins.content LIKE ?', "%#{term}%")
-
end
-
-
query
-
end
-
-
# 统计搜索结果数量
-
def count_search_results(options)
-
perform_search(options).count
-
end
-
-
# 应用排序
-
def apply_sorting(query, options)
-
case options.sort_by
-
when 'relevance'
-
# 相关性排序(简化版)
-
query.order('created_at DESC')
-
when 'created_at'
-
direction = options.sort_direction.upcase == 'ASC' ? 'ASC' : 'DESC'
-
query.order("created_at #{direction}")
-
when 'word_count'
-
direction = options.sort_direction.upcase == 'ASC' ? 'ASC' : 'DESC'
-
query.order("word_count #{direction}")
-
when 'flowers_count'
-
query = query.left_joins(:flowers)
-
.group('check_ins.id')
-
.order("COUNT(flowers.id) #{options.sort_direction.upcase}")
-
else
-
query.order('created_at DESC')
-
end
-
end
-
-
# 生成搜索建议
-
def generate_suggestions(options)
-
suggestions = []
-
-
# 如果没有结果,提供拼写建议
-
if options.query.present? && options.query.length > 2
-
# 简化的拼写检查
-
suggestions << "尝试使用更简短的关键词"
-
suggestions << "检查是否有拼写错误"
-
end
-
-
# 日期范围建议
-
if options.date_from.blank? || options.date_to.blank?
-
suggestions << "添加日期范围以缩小搜索结果"
-
end
-
-
# 关键词建议
-
popular_keywords = popular_keywords(5)
-
if popular_keywords.any?
-
suggestions << "热门关键词:#{popular_keywords.keys.first(3).join(', ')}"
-
end
-
-
suggestions
-
end
-
-
# 生成搜索统计
-
def generate_facets(check_ins)
-
facets = {}
-
-
# 按状态统计
-
status_facet = check_ins.group(:status).count
-
facets[:status] = status_facet.transform_keys { |status| status.to_s }
-
-
# 按日期统计
-
date_facet = check_ins.group('DATE(created_at)').count
-
facets[:dates] = date_facet
-
-
# 按用户统计(前10名)
-
user_facet = check_ins.joins(:user).group('users.nickname').count
-
.sort_by { |_, count| -count }.first(10).to_h
-
facets[:users] = user_facet
-
-
facets
-
end
-
-
# 构建高级查询
-
def build_advanced_query(options)
-
query = CheckIn.joins(:user, :reading_schedule)
-
-
# 实现更复杂的查询逻辑
-
# 例如:OR条件、NOT条件、短语搜索等
-
-
query
-
end
-
end
-
end
-
-
# 扩展CheckIn模型以支持搜索结果格式化
-
class CheckIn
-
def to_search_result_h
-
{
-
id: id,
-
content_preview: content_preview(150),
-
formatted_content: formatted_content(length: 150),
-
user: {
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
},
-
reading_event: {
-
id: reading_event.id,
-
title: reading_event.title
-
},
-
reading_schedule: {
-
id: reading_schedule.id,
-
date: reading_schedule.date,
-
day_number: reading_schedule.day_number
-
},
-
word_count: word_count,
-
status: status,
-
submitted_at: submitted_at,
-
flowers_count: flowers_count,
-
quality_score: quality_score,
-
keywords: keywords(5),
-
reading_time: reading_time_estimate
-
}
-
end
-
end
-
# 每日小红花统计服务
-
# 自动统计前一天的小红花数据,生成排行榜,支持分享功能
-
class DailyFlowerStatsService
-
class << self
-
# 生成指定日期的统计数据(默认为昨天)
-
def generate_daily_stats(event, date = Date.yesterday, force: false)
-
return { success: false, error: '活动不存在' } unless event
-
return { success: false, error: '指定日期不是活动日' } unless event_reading_day?(event, date)
-
-
# 检查是否已存在统计数据
-
if DailyFlowerStat.exists_for_date?(event, date) && !force
-
return { success: false, error: '该日期统计数据已存在' }
-
end
-
-
# 获取前一天的小红花数据
-
flowers = get_flowers_for_date(event, date)
-
return { success: false, error: '该日期无小红花数据' } if flowers.empty?
-
-
# 生成排行榜
-
leaderboard = generate_leaderboard(flowers)
-
-
# 计算统计数据
-
stats_data = calculate_statistics(flowers, event, date)
-
-
# 创建或更新统计记录
-
stat = DailyFlowerStat.find_or_initialize_by(reading_event: event, stats_date: date)
-
stat.update!(
-
leaderboard_data: {
-
rankings: leaderboard,
-
generated_at: Time.current,
-
date: date,
-
flower_count: flowers.count
-
},
-
total_flowers_given: stats_data[:total_flowers_given],
-
total_participants: stats_data[:total_participants],
-
total_givers: stats_data[:total_givers],
-
generated_at: Time.current,
-
generated_by: 'system_auto',
-
share_text: generate_share_text(event, date, leaderboard),
-
share_image_url: generate_share_image_url(event, date)
-
)
-
-
{
-
success: true,
-
message: '每日统计生成成功',
-
stat: stat.as_json_for_api,
-
summary: {
-
date: date,
-
event: event.title,
-
total_flowers: stats_data[:total_flowers_given],
-
total_participants: stats_data[:total_participants],
-
top_three: leaderboard.first(3).map do |entry|
-
user = User.find_by(id: entry[:user_id])
-
{
-
rank: entry[:rank],
-
user: user&.as_json_for_api,
-
total_flowers: entry[:total_flowers]
-
}
-
end
-
}
-
}
-
rescue => e
-
Rails.logger.error "每日统计生成失败: #{e.message}"
-
{
-
success: false,
-
error: '统计生成失败',
-
details: e.message
-
}
-
end
-
-
# 批量生成多日统计(用于历史数据补全)
-
def generate_batch_stats(event, start_date, end_date = nil)
-
return { success: false, error: '活动不存在' } unless event
-
-
end_date ||= event.end_date
-
start_date = [start_date, event.start_date].max
-
-
results = []
-
failed_dates = []
-
-
(start_date..end_date).each do |date|
-
next unless event_reading_day?(event, date)
-
next if date >= Date.current # 不处理今天和未来的日期
-
-
result = generate_daily_stats(event, date, force: false)
-
if result[:success]
-
results << { date: date, success: true }
-
else
-
failed_dates << { date: date, error: result[:error] }
-
end
-
end
-
-
{
-
success: failed_dates.empty?,
-
message: "批量统计完成",
-
results: {
-
processed: results.count,
-
successful: results.count,
-
failed: failed_dates.count,
-
successful_dates: results,
-
failed_dates: failed_dates
-
}
-
}
-
end
-
-
# 自动生成昨天的统计数据(定时任务调用)
-
def auto_generate_yesterday_stats
-
events = ReadingEvent.where(status: [:in_progress, :approved])
-
-
results = []
-
events.each do |event|
-
next unless event_reading_day?(event, Date.yesterday)
-
-
result = generate_daily_stats(event, Date.yesterday, force: false)
-
results << {
-
event_id: event.id,
-
event_title: event.title,
-
date: Date.yesterday,
-
success: result[:success],
-
error: result[:error]
-
}
-
end
-
-
successful = results.select { |r| r[:success] }.count
-
failed = results.count - successful
-
-
Rails.logger.info "自动每日统计完成: 成功 #{successful} 个, 失败 #{failed} 个"
-
-
{
-
success: failed == 0,
-
message: "自动统计完成",
-
summary: {
-
total_events: results.count,
-
successful: successful,
-
failed: failed,
-
results: results
-
}
-
}
-
end
-
-
# 获取活动的每日统计历史
-
def get_event_stats_history(event, days: 30)
-
return { error: '活动不存在' } unless event
-
-
stats = DailyFlowerStat.for_event(event)
-
.where(stats_date: (Date.current - days.days)..Date.current)
-
.order(stats_date: :desc)
-
-
{
-
event: event.as_json_for_api,
-
period: "#{Date.current - days.days} 至 #{Date.current}",
-
stats: stats.map(&:as_json_for_api)
-
}
-
end
-
-
# 获取指定日期的排行榜数据
-
def get_leaderboard_for_date(event, date = Date.yesterday)
-
return { error: '活动不存在' } unless event
-
-
stat = DailyFlowerStat.find_by(reading_event: event, stats_date: date)
-
return { error: '该日期无统计数据' } unless stat
-
-
{
-
success: true,
-
date: date,
-
event: event.as_json_for_api,
-
leaderboard: stat.leaderboard,
-
top_three: stat.top_three,
-
statistics: {
-
total_flowers_given: stat.total_flowers_given,
-
total_participants: stat.total_participants,
-
total_givers: stat.total_givers,
-
share_count: stat.share_count
-
},
-
share_info: {
-
image_url: stat.share_image_url || stat.generate_share_image_url,
-
text: stat.share_text_for_wechat,
-
share_count: stat.share_count
-
},
-
generated_at: stat.generated_at
-
}
-
end
-
-
# 增加分享次数并返回分享信息
-
def increment_share_count(event, date = Date.yesterday)
-
stat = DailyFlowerStat.find_by(reading_event: event, stats_date: date)
-
return { error: '统计数据不存在' } unless stat
-
-
stat.increment_share_count!
-
-
{
-
success: true,
-
share_count: stat.share_count,
-
share_info: {
-
image_url: stat.share_image_url || stat.generate_share_image_url,
-
text: stat.share_text_for_wechat
-
}
-
}
-
end
-
-
# 生成分享图片URL(占位符)
-
def generate_share_image_url(event, date)
-
# 这里可以集成第三方图片生成服务
-
timestamp = Time.current.to_i
-
base_url = Rails.application.config.base_url || 'http://localhost:3000'
-
"#{base_url}/share-images/daily-flower-stats/#{event.id}/#{date}?t=#{timestamp}"
-
end
-
-
private
-
-
# 获取指定日期的小红花数据
-
def get_flowers_for_date(event, date)
-
# 获取指定日期范围内的小红花
-
start_time = date.beginning_of_day
-
end_time = date.end_of_day
-
-
Flower.joins(:recipient)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: event.id })
-
.where('flowers.created_at >= ? AND flowers.created_at <= ?', start_time, end_time)
-
.includes(:giver, :recipient, :check_in)
-
end
-
-
# 生成排行榜
-
def generate_leaderboard(flowers)
-
# 按接收者分组统计小红花数量
-
flower_stats = flowers.group_by(&:recipient_id)
-
.map do |recipient_id, user_flowers|
-
recipient = User.find_by(id: recipient_id)
-
next unless recipient
-
-
{
-
user_id: recipient_id,
-
nickname: recipient.nickname,
-
avatar_url: recipient.avatar_url,
-
total_flowers: user_flowers.sum(&:amount),
-
flowers_received: user_flowers.count,
-
flowers_given: flowers.where(giver_id: recipient_id).count,
-
check_ins: user_flowers.map(&:check_in).uniq.count,
-
last_flower_at: user_flowers.maximum(:created_at)
-
}
-
end
-
.compact
-
.sort_by { |entry| -entry[:total_flowers] }
-
.each_with_index.map { |entry, index| entry.merge(rank: index + 1) }
-
end
-
-
# 计算统计数据
-
def calculate_statistics(flowers, event, date)
-
{
-
total_flowers_given: flowers.sum(&:amount),
-
total_participants: flowers.map(&:recipient_id).uniq.count,
-
total_givers: flowers.map(&:giver_id).uniq.count,
-
average_flowers_per_user: flowers.count > 0 ? (flowers.sum(&:amount).to_f / flowers.map(&:recipient_id).uniq.count).round(2) : 0
-
}
-
end
-
-
# 生成分享文案
-
def generate_share_text(event, date, leaderboard)
-
return '' if leaderboard.empty?
-
-
text = "🌸 #{event.title} #{date.strftime('%m月%d日')}小红花排行榜\n\n"
-
text += "🏆 今日小红花TOP3:\n"
-
-
leaderboard.first(3).each_with_index do |entry, index|
-
emoji = ['🥇', '🥈', '🥉'][index]
-
text += "#{emoji} #{entry[:nickname]} - #{entry[:total_flowers]}朵\n"
-
end
-
-
text += "\n💝 #{leaderboard.first[:total_flowers]}朵小红花来自#{leaderboard.count}位小伙伴的鼓励!"
-
text += "\n#读书打卡 #小红花 #共读成长"
-
-
text
-
end
-
-
# 检查指定日期是否是活动阅读日
-
def event_reading_day?(event, date)
-
return false unless event.start_date && event.end_date
-
return false if date < event.start_date || date > event.end_date
-
-
# 如果设置周末休息,跳过周末
-
if event.weekend_rest && (date.saturday? || date.sunday?)
-
return false
-
end
-
-
true
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# DomainEventsService - 领域事件服务
-
# 负责管理领域事件的发布和订阅,解耦服务间的依赖关系
-
1
class DomainEventsService
-
1
class << self
-
# 发布事件
-
1
def publish(event_name, payload = {})
-
event = DomainEvent.new(event_name, payload)
-
-
Rails.logger.info "发布领域事件: #{event_name} - #{payload.inspect}"
-
-
# 同步执行订阅者
-
ActiveSupport::Notifications.instrument("domain_event.#{event_name}", payload) do
-
subscribers = find_subscribers(event_name)
-
subscribers.each { |subscriber| subscriber.call(event) }
-
end
-
-
event
-
end
-
-
# 订阅事件
-
1
def subscribe(event_name, subscriber_class = nil, &block)
-
9
subscriber = if block_given?
-
block
-
9
elsif subscriber_class
-
9
if subscriber_class.respond_to?(:handle)
-
9
subscriber_class.method(:handle)
-
else
-
raise ArgumentError, "订阅者类必须实现handle方法"
-
end
-
else
-
raise ArgumentError, "必须提供订阅者类或代码块"
-
end
-
-
9
subscribers[event_name] ||= []
-
9
subscribers[event_name] << subscriber
-
-
9
Rails.logger.info "注册事件订阅: #{event_name} -> #{subscriber_class || '匿名订阅者'}"
-
end
-
-
# 取消订阅
-
1
def unsubscribe(event_name, subscriber)
-
subscribers[event_name]&.delete(subscriber)
-
end
-
-
# 获取事件订阅者
-
1
def subscribers_for(event_name)
-
subscribers[event_name] || []
-
end
-
-
# 清除所有订阅者(主要用于测试)
-
1
def clear_subscribers!
-
@subscribers = {}
-
end
-
-
# 获取所有事件类型
-
1
def event_types
-
subscribers.keys
-
end
-
-
1
private
-
-
1
def subscribers
-
18
@subscribers ||= {}
-
end
-
-
1
def find_subscribers(event_name)
-
subscribers[event_name] || []
-
end
-
end
-
-
# 领域事件类
-
1
class DomainEvent
-
1
attr_reader :name, :payload, :timestamp
-
-
1
def initialize(name, payload = {})
-
@name = name
-
@payload = payload.with_indifferent_access
-
@timestamp = Time.current
-
end
-
-
1
def data(key = nil)
-
if key
-
@payload[key]
-
else
-
@payload
-
end
-
end
-
-
1
def occurred_at
-
@timestamp
-
end
-
-
1
def to_s
-
"DomainEvent(#{name}, #{payload}, #{@timestamp})"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# 错误处理服务
-
# 提供统一的错误处理、日志记录和用户友好的错误消息
-
class ErrorHandlingService
-
class << self
-
# 处理API错误并返回标准化响应
-
# @param error [Exception] 错误对象
-
# @param context [Hash] 错误上下文信息
-
# @return [Hash] 标准化的错误响应
-
def handle_api_error(error, context = {})
-
error_info = classify_error(error)
-
-
# 记录错误日志
-
log_error(error, context, error_info)
-
-
# 清除相关的缓存
-
clear_related_cache(context) if error_info[:clear_cache]
-
-
# 发送错误通知(如果是严重错误)
-
notify_error(error, context, error_info) if error_info[:notify]
-
-
# 返回用户友好的错误响应
-
format_error_response(error_info, error, context)
-
end
-
-
# 验证错误处理
-
# @param errors [ActiveModel::Errors] 验证错误对象
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 标准化的验证错误响应
-
def handle_validation_errors(errors, context = {})
-
error_details = errors.details.transform_values do |details|
-
details.map { |detail| detail[:error].to_s.humanize }
-
end
-
-
response = {
-
success: false,
-
error_type: 'validation_error',
-
message: '请求参数验证失败',
-
errors: error_details,
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加请求ID
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
# 记录验证错误日志
-
Rails.logger.warn "验证错误: #{context[:action]} - #{error_details}"
-
-
response
-
end
-
-
# 资源未找到错误处理
-
# @param resource_type [String] 资源类型
-
# @param resource_id [String, Integer] 资源ID
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 标准化的未找到响应
-
def handle_not_found_error(resource_type, resource_id = nil, context = {})
-
message = if resource_id
-
"#{resource_type.humanize} (ID: #{resource_id}) 不存在"
-
else
-
"#{resource_type.humanize} 不存在"
-
end
-
-
response = {
-
success: false,
-
error_type: 'not_found',
-
message: message,
-
resource_type: resource_type,
-
resource_id: resource_id,
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加请求ID
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
# 记录404日志
-
Rails.logger.info "404错误: #{context[:action]} - #{message}"
-
-
response
-
end
-
-
# 权限错误处理
-
# @param action [String] 请求的操作
-
# @param resource [String] 资源信息
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 标准化的权限错误响应
-
def handle_authorization_error(action, resource = nil, context = {})
-
message = if resource
-
"您没有权限执行此操作: #{action} #{resource}"
-
else
-
"您没有权限执行此操作: #{action}"
-
end
-
-
response = {
-
success: false,
-
error_type: 'authorization_error',
-
message: message,
-
required_permission: action,
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加用户信息
-
if context[:user]
-
response[:user_info] = {
-
id: context[:user].id,
-
role: context[:user].role_as_string
-
}
-
end
-
-
# 添加请求ID
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
# 记录权限错误日志
-
Rails.logger.warn "权限错误: #{context[:user]&.id} - #{message}"
-
-
response
-
end
-
-
# 业务逻辑错误处理
-
# @param message [String] 错误消息
-
# @param error_code [String] 错误代码
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 标准化的业务错误响应
-
def handle_business_error(message, error_code = nil, context = {})
-
response = {
-
success: false,
-
error_type: 'business_error',
-
message: message,
-
error_code: error_code,
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加请求ID
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
# 记录业务错误日志
-
Rails.logger.info "业务错误: #{context[:action]} - #{message}"
-
-
response
-
end
-
-
# 服务不可用错误处理
-
# @param service_name [String] 服务名称
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 标准化的服务不可用响应
-
def handle_service_unavailable_error(service_name, context = {})
-
message = "#{service_name} 服务暂时不可用,请稍后再试"
-
-
response = {
-
success: false,
-
error_type: 'service_unavailable',
-
message: message,
-
service_name: service_name,
-
retry_after: 30, # 建议重试时间(秒)
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加请求ID
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
# 记录服务不可用日志
-
Rails.logger.error "服务不可用: #{service_name} - #{context[:action]}"
-
-
response
-
end
-
-
# 限流错误处理
-
# @param limit_info [Hash] 限流信息
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 标准化的限流错误响应
-
def handle_rate_limit_error(limit_info, context = {})
-
response = {
-
success: false,
-
error_type: 'rate_limit_exceeded',
-
message: '请求过于频繁,请稍后再试',
-
limit_info: limit_info,
-
timestamp: Time.current.iso8601
-
}
-
-
# 添加请求ID
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
# 记录限流日志
-
Rails.logger.warn "限流错误: #{context[:action]} - #{limit_info}"
-
-
response
-
end
-
-
# 创建用户友好的错误消息
-
# @param error_class [Class] 错误类
-
# @param error_message [String] 原始错误消息
-
# @param context [Hash] 上下文信息
-
# @return [String] 用户友好的错误消息
-
def create_user_friendly_message(error_class, error_message, context = {})
-
case error_class.name
-
when 'ActiveRecord::RecordNotFound'
-
case context[:resource_type]
-
when 'User'
-
'用户不存在'
-
when 'ReadingEvent'
-
'活动不存在'
-
when 'CheckIn'
-
'打卡记录不存在'
-
when 'Flower'
-
'小红花不存在'
-
else
-
'记录不存在'
-
end
-
when 'ActiveRecord::RecordInvalid'
-
'数据验证失败,请检查输入信息'
-
when 'ActiveRecord::RecordNotSaved'
-
'保存失败,请检查网络连接后重试'
-
when 'ArgumentError'
-
'请求参数不正确'
-
when 'JWT::DecodeError'
-
'登录信息无效,请重新登录'
-
when 'NoMethodError'
-
'功能暂时不可用'
-
when 'StandardError'
-
if error_message.include?('数据库') || error_message.include?('database')
-
'数据服务暂时不可用,请稍后再试'
-
elsif error_message.include?('网络') || error_message.include?('network')
-
'网络连接异常,请检查网络后重试'
-
elsif error_message.include?('超时') || error_message.include?('timeout')
-
'请求超时,请稍后再试'
-
else
-
'系统暂时异常,请稍后再试'
-
end
-
else
-
'系统暂时异常,请稍后再试'
-
end
-
end
-
-
# 错误分类
-
# @param error [Exception] 错误对象
-
# @return [Hash] 错误分类信息
-
def classify_error(error)
-
base_info = {
-
class_name: error.class.name,
-
message: error.message,
-
backtrace: error.backtrace&.first(5)
-
}
-
-
case error
-
when ActiveRecord::RecordNotFound
-
base_info.merge(
-
severity: :low,
-
user_friendly: true,
-
http_status: 404,
-
clear_cache: false,
-
notify: false
-
)
-
when ActiveRecord::RecordInvalid, ArgumentError
-
base_info.merge(
-
severity: :low,
-
user_friendly: true,
-
http_status: 400,
-
clear_cache: false,
-
notify: false
-
)
-
when JWT::DecodeError, ActionController::InvalidAuthenticityToken
-
base_info.merge(
-
severity: :medium,
-
user_friendly: true,
-
http_status: 401,
-
clear_cache: false,
-
notify: false
-
)
-
when ActiveRecord::RecordNotSaved, ActiveRecord::StatementInvalid
-
base_info.merge(
-
severity: :medium,
-
user_friendly: true,
-
http_status: 422,
-
clear_cache: false,
-
notify: true
-
)
-
when StandardError
-
if error.message.include?('超时') || error.message.include?('timeout')
-
base_info.merge(
-
severity: :medium,
-
user_friendly: true,
-
http_status: 408,
-
clear_cache: false,
-
notify: false
-
)
-
elsif error.message.include?('权限') || error.message.include?('permission')
-
base_info.merge(
-
severity: :medium,
-
user_friendly: true,
-
http_status: 403,
-
clear_cache: false,
-
notify: false
-
)
-
else
-
base_info.merge(
-
severity: :high,
-
user_friendly: false,
-
http_status: 500,
-
clear_cache: true,
-
notify: true
-
)
-
end
-
else
-
base_info.merge(
-
severity: :high,
-
user_friendly: false,
-
http_status: 500,
-
clear_cache: true,
-
notify: true
-
)
-
end
-
end
-
-
# 记录错误日志
-
# @param error [Exception] 错误对象
-
# @param context [Hash] 上下文信息
-
# @param error_info [Hash] 错误分类信息
-
def log_error(error, context, error_info)
-
log_data = {
-
error_class: error_info[:class_name],
-
error_message: error_info[:message],
-
severity: error_info[:severity],
-
context: context,
-
timestamp: Time.current,
-
backtrace: error_info[:backtrace]
-
}
-
-
# 添加用户信息
-
if context[:user]
-
log_data[:user_id] = context[:user].id
-
log_data[:user_role] = context[:user].role_as_string
-
end
-
-
# 根据严重程度选择日志级别
-
case error_info[:severity]
-
when :low
-
Rails.logger.info "错误日志: #{log_data}"
-
when :medium
-
Rails.logger.warn "警告日志: #{log_data}"
-
when :high
-
Rails.logger.error "错误日志: #{log_data}"
-
end
-
-
# 发送到外部错误监控服务
-
send_to_error_monitoring(log_data) if error_info[:notify]
-
end
-
-
# 清除相关缓存
-
# @param context [Hash] 上下文信息
-
def clear_related_cache(context)
-
return unless context[:user]
-
-
case context[:action]
-
when 'create', 'update', 'destroy'
-
CacheService.clear_user_cache(context[:user])
-
end
-
end
-
-
# 发送错误通知
-
# @param error [Exception] 错误对象
-
# @param context [Hash] 上下文信息
-
# @param error_info [Hash] 错误分类信息
-
def notify_error(error, context, error_info)
-
return unless Rails.env.production? # 只在生产环境发送通知
-
-
# 这里可以集成邮件、Slack、钉钉等通知服务
-
error_data = {
-
error_class: error_info[:class_name],
-
error_message: error_info[:message],
-
context: context,
-
timestamp: Time.current,
-
environment: Rails.env
-
}
-
-
# 示例:发送到Slack(需要配置webhook)
-
# SlackNotifier.notify_error(error_data) if defined?(SlackNotifier)
-
end
-
-
# 格式化错误响应
-
# @param error_info [Hash] 错误分类信息
-
# @param error [Exception] 错误对象
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 标准化的错误响应
-
def format_error_response(error_info, error, context)
-
user_friendly_message = create_user_friendly_message(
-
error.class,
-
error_info[:message],
-
context
-
)
-
-
response = {
-
success: false,
-
error_type: error_info[:class_name].underscore,
-
message: user_friendly_message,
-
timestamp: Time.current.iso8601
-
}
-
-
# 开发环境显示详细信息
-
if Rails.env.development?
-
response[:debug] = {
-
original_error: error_info[:message],
-
backtrace: error_info[:backtrace],
-
context: context
-
}
-
end
-
-
# 添加请求ID
-
if RequestStore.store[:request_id]
-
response[:request_id] = RequestStore.store[:request_id]
-
end
-
-
response
-
end
-
-
# 发送错误到外部监控服务
-
# @param error_data [Hash] 错误数据
-
def send_to_error_monitoring(error_data)
-
# 这里可以集成Sentry、Bugsnag、Rollbar等错误监控服务
-
# 示例:
-
# Sentry.capture_exception(error_data[:error], extra: error_data)
-
end
-
-
# 异常处理装饰器
-
# @param operation [Symbol] 操作类型
-
# @param context [Hash] 上下文信息
-
# @param options [Hash] 选项
-
# @yield 要执行的操作
-
# @return [Object] 操作结果或错误响应
-
def with_error_handling(operation, context = {}, options = {})
-
begin
-
yield
-
rescue => e
-
if options[:return_response]
-
handle_api_error(e, context.merge(operation: operation))
-
else
-
raise e
-
end
-
end
-
end
-
-
# 批量错误处理
-
# @param operations [Array] 操作数组
-
# @param context [Hash] 上下文信息
-
# @return [Hash] 批量处理结果
-
def handle_batch_errors(operations, context = {})
-
results = {
-
successful: [],
-
failed: [],
-
total: operations.length
-
}
-
-
operations.each_with_index do |operation, index|
-
begin
-
result = yield(operation) if block_given?
-
results[:successful] << {
-
index: index,
-
operation: operation,
-
result: result
-
}
-
rescue => e
-
error_response = handle_api_error(e, context.merge(operation: operation))
-
results[:failed] << {
-
index: index,
-
operation: operation,
-
error: error_response
-
}
-
end
-
end
-
-
results
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# EventEnrollmentService - 活动报名管理服务
-
# 负责活动报名、验证、人数限制等业务逻辑
-
class EventEnrollmentService < ApplicationService
-
attr_reader :event, :user, :enrollment
-
-
def initialize(event:, user:)
-
super()
-
@event = event
-
@user = user
-
@enrollment = nil
-
end
-
-
# 主要调用方法
-
def call
-
handle_errors do
-
# 检查活动是否已审批
-
unless event.approved?
-
return failure!("活动尚未审批通过,无法报名")
-
end
-
-
# 检查是否已报名
-
if user.enrollments.exists?(reading_event: event)
-
return failure!("您已经报名该活动")
-
end
-
-
# 检查人数限制
-
if event.enrollments.count >= event.max_participants
-
return failure!("活动已满员")
-
end
-
-
# 检查活动状态
-
unless event.enrolling?
-
return failure!("当前活动不在报名期间")
-
end
-
-
# 创建报名记录
-
create_enrollment
-
end
-
end
-
-
# 类方法:快速报名
-
def self.enroll_user!(event, user)
-
new(event: event, user: user).call
-
end
-
-
private
-
-
# 创建报名记录
-
def create_enrollment
-
@enrollment = user.enrollments.create!(
-
reading_event: event,
-
paid_amount: event.enrollment_fee
-
)
-
-
# 如果是随机分配模式且有足够参与者,自动分配领读人
-
if event.leader_assignment_type == 'random' && event.enrollments.count >= 3
-
event.assign_daily_leaders!
-
end
-
-
success!({
-
message: "报名成功",
-
enrollment_data: {
-
id: @enrollment.id,
-
user_id: @enrollment.user_id,
-
reading_event_id: @enrollment.reading_event_id,
-
payment_status: @enrollment.payment_status,
-
role: @enrollment.role,
-
paid_amount: @enrollment.paid_amount,
-
created_at: @enrollment.created_at
-
}
-
})
-
end
-
end
-
# frozen_string_literal: true
-
-
# EventManagementService - 活动生命周期管理服务
-
# 负责活动的创建、审批、拒绝、完成等核心业务逻辑
-
class EventManagementService < ApplicationService
-
attr_reader :event, :admin_user, :action
-
-
def initialize(event:, admin_user: nil, action: nil)
-
super()
-
@event = event
-
@admin_user = admin_user
-
@action = action
-
end
-
-
# 主要调用方法
-
def call
-
handle_errors do
-
case action
-
when :approve
-
approve_event
-
when :reject
-
reject_event
-
when :complete
-
complete_event
-
else
-
failure!("不支持的操作: #{action}")
-
end
-
end
-
self # 返回service实例
-
end
-
-
# 类方法:审批活动
-
def self.approve_event!(event, admin_user)
-
new(event: event, admin_user: admin_user, action: :approve).call
-
end
-
-
# 类方法:拒绝活动
-
def self.reject_event!(event, admin_user)
-
new(event: event, admin_user: admin_user, action: :reject).call
-
end
-
-
# 类方法:完成活动
-
def self.complete_event!(event, current_user)
-
new(event: event, admin_user: current_user, action: :complete).call
-
end
-
-
private
-
-
# 审批活动
-
def approve_event
-
return failure!("管理员用户不能为空") unless admin_user
-
-
# 检查管理员权限
-
unless admin_user.can_approve_events?
-
return failure!("用户 #{admin_user.nickname} 没有审批权限")
-
end
-
-
# 检查活动状态
-
unless event.pending_approval?
-
return failure!("只能审批待审批的活动")
-
end
-
-
# 执行审批
-
event.transaction do
-
event.approve!(admin_user)
-
-
# 如果是随机分配模式且有足够参与者,自动分配领读人
-
if event.leader_assignment_type == 'random' && event.enrollments.count >= 3
-
event.assign_daily_leaders!
-
end
-
end
-
-
success!({
-
message: "活动审批通过",
-
event_data: {
-
'id' => event.id,
-
'title' => event.title,
-
'status' => event.status_symbol,
-
'approval_status' => event.approval_status_symbol,
-
'approved_by' => admin_user.nickname,
-
'approved_at' => event.approved_at
-
}
-
})
-
end
-
-
# 拒绝活动
-
def reject_event
-
return failure!("管理员用户不能为空") unless admin_user
-
-
# 检查管理员权限
-
unless admin_user.can_approve_events?
-
return failure!("用户 #{admin_user.nickname} 没有审批权限")
-
end
-
-
# 检查活动状态
-
unless event.pending_approval?
-
return failure!("只能拒绝待审批的活动")
-
end
-
-
# 执行拒绝
-
event.reject!(admin_user)
-
-
success!({
-
message: "活动已被拒绝",
-
event_data: {
-
'id' => event.id,
-
'title' => event.title,
-
'status' => event.status_symbol,
-
'approval_status' => event.approval_status_symbol,
-
'rejected_by' => admin_user.nickname,
-
'rejected_at' => event.approved_at
-
}
-
})
-
end
-
-
# 完成活动
-
def complete_event
-
# 检查活动状态(先检查状态,再检查权限)
-
if event.completed?
-
return failure!("活动已经结束")
-
end
-
-
# 检查用户权限 - 只有活动小组长可以结束活动
-
unless event.current_leader?(admin_user)
-
return failure!("只有活动小组长可以结束活动")
-
end
-
-
# 执行活动完成
-
event.complete_event!
-
-
success!({
-
message: "活动已成功结束",
-
event_data: {
-
'id' => event.id,
-
'title' => event.title,
-
'status' => event.status_symbol,
-
'completed_at' => Time.current
-
}
-
})
-
end
-
end
-
# frozen_string_literal: true
-
-
# NotificationEventSubscriber - 通知事件订阅者
-
# 监听各种领域事件并发送相应的通知
-
1
class NotificationEventSubscriber
-
1
class << self
-
# 处理领域事件
-
1
def handle(event)
-
case event.name
-
when 'flower.given'
-
handle_flower_given(event)
-
when 'flower.comment_created'
-
handle_flower_comment(event)
-
when 'post.created'
-
handle_post_created(event)
-
when 'post.updated'
-
handle_post_updated(event)
-
when 'post.moderated'
-
handle_post_moderated(event)
-
when 'report.created'
-
handle_report_created(event)
-
when 'report.processed'
-
handle_report_processed(event)
-
when 'event.enrollment.created'
-
handle_event_enrollment_created(event)
-
when 'event.approval.required'
-
handle_event_approval_required(event)
-
else
-
Rails.logger.warn "未知事件类型: #{event.name}"
-
end
-
rescue => e
-
Rails.logger.error "处理事件失败 #{event.name}: #{e.message}"
-
Rails.logger.error e.backtrace.join("\n")
-
end
-
-
1
private
-
-
# 处理小红花赠送事件
-
1
def handle_flower_given(event)
-
giver = event.data(:giver)
-
recipient = event.data(:recipient)
-
flower = event.data(:flower)
-
-
return unless giver && recipient && flower
-
-
NotificationService.send_flower_notification(recipient, giver, flower)
-
end
-
-
# 处理小红花评论事件
-
1
def handle_flower_comment(event)
-
flower = event.data(:flower)
-
commenter = event.data(:commenter)
-
comment = event.data(:comment)
-
-
return unless flower && commenter && comment
-
-
NotificationService.send_comment_notification(flower.recipient, commenter, comment)
-
end
-
-
# 处理帖子创建事件
-
1
def handle_post_created(event)
-
post = event.data(:post)
-
user = event.data(:user)
-
-
return unless post && user
-
-
# 可以在这里发送帖子创建通知给关注者等
-
# NotificationService.post_created_notification(post, user)
-
Rails.logger.info "帖子创建事件: #{post.title} by #{user.nickname}"
-
end
-
-
# 处理帖子更新事件
-
1
def handle_post_updated(event)
-
post = event.data(:post)
-
user = event.data(:user)
-
-
return unless post && user
-
-
# NotificationService.post_updated_notification(post, user)
-
Rails.logger.info "帖子更新事件: #{post.title} by #{user.nickname}"
-
end
-
-
# 处理帖子审核事件
-
1
def handle_post_moderated(event)
-
post = event.data(:post)
-
moderator = event.data(:moderator)
-
action = event.data(:action)
-
reason = event.data(:reason)
-
-
return unless post && moderator
-
-
case action
-
when 'pin'
-
# NotificationService.post_pinned_notification(post, moderator)
-
Rails.logger.info "帖子置顶事件: #{post.title} by #{moderator.nickname}"
-
when 'hide'
-
# NotificationService.post_hidden_notification(post, moderator, reason)
-
Rails.logger.info "帖子隐藏事件: #{post.title} by #{moderator.nickname}, 原因: #{reason}"
-
when 'delete'
-
# NotificationService.post_deleted_notification(post, moderator, reason)
-
Rails.logger.info "帖子删除事件: #{post.title} by #{moderator.nickname}, 原因: #{reason}"
-
end
-
end
-
-
# 处理举报创建事件
-
1
def handle_report_created(event)
-
report = event.data(:report)
-
reporter = event.data(:reporter)
-
-
return unless report && reporter
-
-
# 发送通知给管理员
-
NotificationService.send_bulk_notifications(
-
User.where(role: %w[admin moderator]),
-
reporter,
-
report,
-
'report_created',
-
'新的举报',
-
"用户 #{reporter.nickname} 提交了新的举报,请及时处理。"
-
)
-
end
-
-
# 处理举报处理事件
-
1
def handle_report_processed(event)
-
report = event.data(:report)
-
processor = event.data(:processor)
-
action = event.data(:action)
-
-
return unless report && processor
-
-
# 通知举报者处理结果
-
if report.user
-
NotificationService.send_system_notification(
-
report.user,
-
'举报处理结果',
-
"您提交的举报已被处理,处理结果:#{action}",
-
actor: processor,
-
notifiable: report
-
)
-
end
-
end
-
-
# 处理活动报名事件
-
1
def handle_event_enrollment_created(event)
-
enrollment = event.data(:enrollment)
-
user = event.data(:user)
-
event = event.data(:event)
-
-
return unless enrollment && user && event
-
-
# 通知活动组织者
-
NotificationService.send_activity_update_notification(
-
event.user, # 活动创建者
-
user,
-
event,
-
'new_enrollment',
-
"#{user.nickname} 报名了您的活动"
-
)
-
end
-
-
# 处理活动审批需求事件
-
1
def handle_event_approval_required(event)
-
event = event.data(:event)
-
submitter = event.data(:submitter)
-
-
return unless event && submitter
-
-
# 通知所有管理员审批
-
NotificationService.send_bulk_notifications(
-
User.where(role: 'admin'),
-
submitter,
-
event,
-
'event_approval_required',
-
'活动审批',
-
"活动 #{event.title} 需要审批"
-
)
-
end
-
end
-
end
-
# 小红花证书服务
-
# 负责生成和管理小红花相关的证书
-
class FlowerCertificateService
-
class << self
-
# 活动结束时生成小红花证书
-
def finalize_event_flower_certificates(event)
-
return { success: false, error: '活动未结束' } unless event.status == 'completed'
-
return { success: false, error: '活动没有参与者' } if event.participants.empty?
-
-
certificates = generate_top_three_certificates(event)
-
-
{
-
success: true,
-
event: event.title,
-
certificates: certificates.map do |cert|
-
{
-
rank: cert.rank_display,
-
user: cert.user.as_json_for_api,
-
total_flowers: cert.total_flowers,
-
certificate_id: cert.certificate_id,
-
honor_level: cert.honor_level,
-
share_url: cert.share_url
-
}
-
end
-
}
-
end
-
-
# 生成活动前三名证书
-
def generate_top_three_certificates(event)
-
return [] unless event.participants.any?
-
-
# 计算每个参与者的小红花总数
-
flower_stats = calculate_event_flower_statistics(event)
-
-
# 排序并取前三名
-
top_three = flower_stats.sort_by { |user_id, flowers| -flowers }
-
.first(3)
-
-
certificates = []
-
top_three.each_with_index do |(user_id, flowers), index|
-
user = User.find(user_id)
-
rank = index + 1
-
-
# 生成证书
-
cert = FlowerCertificate.create!(
-
user: user,
-
reading_event: event,
-
certificate_type: "flower_top#{rank}",
-
rank: rank,
-
total_flowers: flowers,
-
certificate_number: generate_certificate_number(event, rank),
-
honor_level: calculate_honor_level(flowers),
-
issued_at: Time.current,
-
expires_at: event.end_date + 1.year
-
)
-
-
certificates << cert
-
-
# 记录到参与者的证书列表
-
participation_cert = ParticipationCertificate.create!(
-
user: user,
-
reading_event: event,
-
certificate_type: "flower_top#{rank}",
-
certificate_number: cert.certificate_number,
-
issued_at: cert.issued_at
-
)
-
-
# 发送通知
-
send_certificate_notification(user, cert, event)
-
end
-
-
certificates
-
end
-
-
# 获取活动的前三名排行榜
-
def get_event_top_three(event)
-
return { error: '活动未结束' } unless event.status == 'completed'
-
-
certificates = FlowerCertificate.for_event(event).ranked
-
-
{
-
event: event.title,
-
total_participants: event.participants.count,
-
top_three: certificates.map do |cert|
-
{
-
rank: cert.rank_display,
-
user: cert.user.as_json_for_api,
-
total_flowers: cert.total_flowers,
-
honor_level: cert.honor_level,
-
certificate_id: cert.certificate_id
-
}
-
end,
-
generated_at: certificates.first&.created_at
-
}
-
end
-
-
# 获取用户的所有小红花证书
-
def get_user_certificates(user)
-
certificates = FlowerCertificate.for_user_all(user)
-
-
{
-
user: user.as_json_for_api,
-
total_certificates: certificates.count,
-
certificates: certificates.map do |cert|
-
{
-
event: cert.reading_event.title,
-
rank: cert.rank_display,
-
total_flowers: cert.total_flowers,
-
honor_level: cert.honor_level,
-
certificate_id: cert.certificate_id,
-
earned_at: cert.created_at,
-
is_valid: cert.valid_certificate?,
-
share_url: cert.share_url
-
}
-
end
-
}
-
end
-
-
# 验证证书有效性
-
def validate_certificate(certificate_id)
-
cert = FlowerCertificate.find_by(certificate_id: certificate_id)
-
return { valid: false, error: '证书不存在' } unless cert
-
-
{
-
valid: cert.valid_certificate?,
-
certificate: cert,
-
user: cert.user.as_json_for_api,
-
event: cert.reading_event.as_json_for_api,
-
expires_at: cert.expires_at,
-
days_until_expiry: cert.days_until_expiry
-
}
-
end
-
-
# 重新生成证书(用于修正错误)
-
def regenerate_certificate(certificate_id, admin_user)
-
cert = FlowerCertificate.find_by(certificate_id: certificate_id)
-
return { success: false, error: '证书不存在' } unless cert
-
-
# 记录重新生成日志
-
Rails.logger.info "证书重新生成: #{certificate_id} by #{admin_user&.nickname}"
-
-
# 生成新的证书编号
-
new_certificate_number = generate_certificate_number(cert.reading_event, cert.rank)
-
-
cert.update!(
-
certificate_number: new_certificate_number,
-
issued_at: Time.current,
-
expires_at: cert.reading_event.end_date + 1.year,
-
regenerated_at: Time.current,
-
regenerated_by: admin_user&.id
-
)
-
-
{
-
success: true,
-
certificate: cert,
-
message: '证书已重新生成'
-
}
-
end
-
-
# 批量生成参与证书
-
def batch_generate_participation_certificates(event, user_ids = nil)
-
return { success: false, error: '活动未结束' } unless event.status == 'completed'
-
-
target_users = user_ids ? User.where(id: user_ids) : event.participants
-
-
certificates = []
-
target_users.each do |user|
-
enrollment = event.event_enrollments.find_by(user: user)
-
next unless enrollment&.is_completed?
-
-
# 生成完成证书
-
cert = ParticipationCertificate.create!(
-
user: user,
-
reading_event: event,
-
certificate_type: 'completion',
-
certificate_number: generate_certificate_number(event, 'completion'),
-
issued_at: Time.current,
-
expires_at: event.end_date + 2.years
-
)
-
-
certificates << cert
-
end
-
-
{
-
success: true,
-
generated_count: certificates.count,
-
certificates: certificates
-
}
-
end
-
-
private
-
-
# 计算活动中每个参与者的小红花统计
-
def calculate_event_flower_statistics(event)
-
flower_stats = {}
-
-
event.check_ins.includes(:flowers, :user).each do |check_in|
-
check_in.flowers.each do |flower|
-
user_id = flower.recipient_id
-
flower_stats[user_id] = (flower_stats[user_id] || 0) + flower.amount
-
end
-
end
-
-
flower_stats
-
end
-
-
# 生成证书编号
-
def generate_certificate_number(event, type_or_rank)
-
prefix = event.id.to_s.rjust(4, '0')
-
timestamp = Time.current.strftime('%Y%m%d')
-
type_code = type_or_rank.is_a?(Integer) ? "TOP#{type_or_rank}" : type_or_rank.to_s.upcase.first(3)
-
random_code = SecureRandom.hex(4).upcase
-
-
"#{prefix}-#{timestamp}-#{type_code}-#{random_code}"
-
end
-
-
# 计算荣誉等级
-
def calculate_honor_level(flowers)
-
case flowers
-
when 0..2
-
'bronze'
-
when 3..5
-
'silver'
-
when 6..10
-
'gold'
-
else
-
'platinum'
-
end
-
end
-
-
# 发送证书通知
-
def send_certificate_notification(user, certificate, event)
-
# 这里应该调用通知服务发送邮件或消息
-
# NotificationService.send_certificate_notification(user, certificate, event)
-
-
Rails.logger.info "证书通知已发送: 用户#{user.nickname}, 证书#{certificate.certificate_id}"
-
end
-
end
-
end
-
# 小红花评论服务
-
# 负责管理小红花的评论功能,包括创建、查询和权限管理
-
class FlowerCommentService
-
class << self
-
# 为小红花添加评论
-
def add_comment_to_flower(flower, user, content)
-
return { success: false, error: '小红花不存在' } unless flower
-
return { success: false, error: '用户不存在' } unless user
-
return { success: false, error: '评论内容不能为空' } if content.blank?
-
-
# 验证评论权限
-
unless can_comment_on_flower?(flower, user)
-
return { success: false, error: '您没有权限评论此小红花' }
-
end
-
-
# 内容验证
-
unless valid_comment_content?(content)
-
return { success: false, error: '评论内容长度应在2-1000字符之间' }
-
end
-
-
# 创建评论
-
comment = flower.add_comment(user, content)
-
-
# 发布小红花评论事件,解耦通知服务
-
DomainEventsService.publish('flower.comment_created', {
-
flower: flower,
-
commenter: user,
-
comment: comment
-
})
-
-
{
-
success: true,
-
comment: comment.as_json_for_api,
-
message: '评论添加成功'
-
}
-
rescue => e
-
Rails.logger.error "小红花评论添加失败: #{e.message}"
-
{
-
success: false,
-
error: '评论添加失败,请重试',
-
details: e.message
-
}
-
end
-
-
# 获取小红花的评论列表
-
def get_flower_comments(flower, page = 1, limit = 10, current_user: nil)
-
return { success: false, error: '小红花不存在' } unless flower
-
-
# 分页查询评论
-
comments = flower.comments
-
.includes(:user)
-
.order(created_at: :desc)
-
.offset((page - 1) * limit)
-
.limit(limit)
-
-
# 检查用户权限
-
can_comment = current_user ? can_comment_on_flower?(flower, current_user) : false
-
-
{
-
success: true,
-
flower: {
-
id: flower.id,
-
giver_display_name: flower.giver_display_name,
-
recipient_display_name: flower.recipient_display_name,
-
flower_type: flower.flower_type,
-
created_at: flower.created_at
-
},
-
comments: comments.map do |comment|
-
comment_data = comment.as_json_for_api
-
comment_data[:can_edit] = current_user ? comment.can_edit?(current_user) : false
-
comment_data
-
end,
-
pagination: {
-
current_page: page,
-
total_count: flower.comments_count,
-
total_pages: (flower.comments_count.to_f / limit).ceil,
-
has_next: (page * limit) < flower.comments_count,
-
has_prev: page > 1
-
},
-
permissions: {
-
can_comment: can_comment,
-
total_comments: flower.comments_count
-
}
-
}
-
end
-
-
# 获取小红花的评论统计
-
def get_flower_comment_stats(flower)
-
return { success: false, error: '小红花不存在' } unless flower
-
-
comments = flower.comments.includes(:user)
-
-
# 计算统计数据
-
stats = {
-
total_count: comments.count,
-
today_count: comments.where(created_at: Date.current.all_day).count,
-
this_week_count: comments.where(created_at: Date.current.beginning_of_week..Date.current.end_of_week).count,
-
unique_users: comments.distinct.count(:user_id),
-
avg_comment_length: comments.average("LENGTH(content)")&.round(2) || 0
-
}
-
-
# 最活跃的评论者
-
active_commenters = comments.joins(:user)
-
.group('users.id', 'users.nickname')
-
.order('COUNT(*) DESC')
-
.limit(5)
-
.count
-
-
{
-
success: true,
-
flower_id: flower.id,
-
stats: stats,
-
active_commenters: active_commenters.map { |user_id, nickname, count|
-
{
-
user_id: user_id,
-
nickname: nickname,
-
comment_count: count
-
}
-
},
-
latest_comment: comments.order(created_at: :desc).first&.as_json_for_api
-
}
-
end
-
-
# 删除小红花评论
-
def delete_flower_comment(flower, comment, current_user)
-
return { success: false, error: '评论不存在' } unless comment
-
return { success: false, error: '小红花不存在' } unless flower
-
return { success: false, error: '用户不存在' } unless current_user
-
-
# 检查删除权限
-
unless can_delete_comment?(comment, current_user)
-
return { success: false, error: '您没有权限删除此评论' }
-
end
-
-
# 删除评论
-
comment.destroy
-
-
{
-
success: true,
-
message: '评论已删除',
-
remaining_comments: flower.comments_count
-
}
-
rescue => e
-
Rails.logger.error "小红花评论删除失败: #{e.message}"
-
{
-
success: false,
-
error: '评论删除失败,请重试',
-
details: e.message
-
}
-
end
-
-
# 批量删除小红花评论(管理员功能)
-
def batch_delete_flower_comments(flower, comment_ids, admin_user)
-
return { success: false, error: '需要管理员权限' } unless admin_user&.any_admin?
-
return { success: false, error: '小红花不存在' } unless flower
-
-
# 查找要删除的评论
-
comments = flower.comments.where(id: comment_ids)
-
-
deleted_count = 0
-
failed_comments = []
-
-
comments.each do |comment|
-
if comment.destroy
-
deleted_count += 1
-
else
-
failed_comments << comment.id
-
end
-
end
-
-
{
-
success: failed_comments.empty?,
-
deleted_count: deleted_count,
-
failed_count: failed_comments.length,
-
failed_comment_ids: failed_comments,
-
remaining_comments: flower.comments_count,
-
message: failed_comments.empty? ? "所有评论已删除" : "部分评论删除失败"
-
}
-
rescue => e
-
Rails.logger.error "批量删除小红花评论失败: #{e.message}"
-
{
-
success: false,
-
error: '批量删除失败,请重试',
-
details: e.message
-
}
-
end
-
-
# 搜索小红花评论
-
def search_flower_comments(flower, keyword, page = 1, limit = 10, current_user: nil)
-
return { success: false, error: '小红花不存在' } unless flower
-
return { success: false, error: '搜索关键词不能为空' } if keyword.blank?
-
-
# 搜索评论
-
comments = flower.comments
-
.includes(:user)
-
.where('content ILIKE ?', "%#{keyword}%")
-
.order(created_at: :desc)
-
.offset((page - 1) * limit)
-
.limit(limit)
-
-
{
-
success: true,
-
keyword: keyword,
-
results: comments.map do |comment|
-
comment_data = comment.as_json_for_api
-
comment_data[:can_edit] = current_user ? comment.can_edit?(current_user) : false
-
comment_data[:highlighted_content] = highlight_search_content(comment.content, keyword)
-
comment_data
-
end,
-
pagination: {
-
current_page: page,
-
total_count: comments.count,
-
total_pages: (comments.count.to_f / limit).ceil,
-
has_next: (page * limit) < comments.count,
-
has_prev: page > 1
-
}
-
}
-
rescue => e
-
Rails.logger.error "小红花评论搜索失败: #{e.message}"
-
{
-
success: false,
-
error: '搜索失败,请重试',
-
details: e.message
-
}
-
end
-
-
private
-
-
# 检查用户是否可以评论小红花
-
def can_comment_on_flower?(flower, user)
-
return false unless user && flower
-
-
# 小红花接收者可以评论
-
return true if flower.recipient_id == user.id
-
-
# 小红花赠送者可以评论
-
return true if flower.giver_id == user.id
-
-
# 同一活动的参与者可以评论
-
if flower.check_in && flower.check_in.reading_event
-
event = flower.check_in.reading_event
-
return true if event.participants.include?(user)
-
end
-
-
false
-
end
-
-
# 检查用户是否可以删除评论
-
def can_delete_comment?(comment, user)
-
return false unless user && comment
-
-
# 评论作者可以删除自己的评论
-
return true if comment.user_id == user.id
-
-
# 管理员可以删除任何评论
-
return true if user.any_admin?
-
-
# 小红花接收者可以删除关于自己的小红花的评论
-
if comment.commentable_type == 'Flower'
-
flower = comment.commentable
-
return true if flower.recipient_id == user.id
-
end
-
-
false
-
end
-
-
# 验证评论内容
-
def valid_comment_content?(content)
-
content_length = content.to_s.strip.length
-
content_length >= 2 && content_length <= 1000
-
end
-
-
# 高亮搜索关键词
-
def highlight_search_content(content, keyword)
-
# 简单的关键词高亮实现
-
content.gsub(/#{Regexp.escape(keyword)}/i, "**#{keyword}**")
-
end
-
-
# 发送小红花评论通知已移至事件订阅者中处理
-
# 这样可以解耦FlowerCommentService和NotificationService的依赖关系
-
end
-
end
-
# 小红花赠送服务
-
# 负责处理小红花赠送流程,包括配额检查和确认机制
-
class FlowerGivingService
-
class << self
-
# 尝试赠送小红花(带每日配额检查和确认提示)
-
def give_flower_with_confirmation(giver, recipient, check_in, amount: 1, comment: nil,
-
flower_type: 'regular', is_anonymous: false, confirmed: false)
-
# 获取活动和日期
-
event = check_in.reading_event rescue nil
-
return { success: false, error: '无法确定活动' } unless event
-
-
date = Date.current
-
-
# 检查是否是活动日
-
unless FlowerQuotaService.activity_day?(event, date)
-
return { success: false, error: '今日不是活动日,无法赠送小红花' }
-
end
-
-
# 检查是否给自己赠送
-
if giver.id == recipient.id
-
return { success: false, error: '不能给自己赠送小红花' }
-
end
-
-
# 获取每日配额
-
quota = FlowerQuotaService.get_daily_quota(giver, event, date)
-
-
# 检查配额
-
unless quota.can_give_flower?(amount)
-
return {
-
success: false,
-
error: '今日小红花配额已用完',
-
remaining: quota.remaining_flowers,
-
max: quota.max_flowers,
-
used: quota.used_flowers
-
}
-
end
-
-
# 如果未确认,返回确认信息
-
unless confirmed
-
return {
-
success: true,
-
require_confirmation: true,
-
confirmation_data: {
-
giver: giver.as_json_for_api,
-
recipient: recipient.as_json_for_api,
-
check_in: {
-
id: check_in.id,
-
content: check_in.content.truncate(100),
-
user: check_in.user.as_json_for_api
-
},
-
amount: amount,
-
comment: comment,
-
flower_type: flower_type,
-
is_anonymous: is_anonymous,
-
date: date,
-
quota_info: {
-
used: quota.used_flowers,
-
max: quota.max_flowers,
-
remaining: quota.remaining_flowers
-
},
-
warning: '赠送成功后无法撤回,请谨慎确认!'
-
}
-
}
-
end
-
-
# 使用事务确保数据一致性
-
ActiveRecord::Base.transaction do
-
# 扣减配额
-
unless FlowerQuotaService.use_quota!(giver, event, amount, date)
-
raise '配额使用失败'
-
end
-
-
# 创建小红花记录
-
flower = Flower.create!(
-
giver: giver,
-
recipient: recipient,
-
check_in: check_in,
-
amount: amount,
-
flower_type: flower_type,
-
comment: comment,
-
is_anonymous: is_anonymous
-
)
-
-
# 更新配额的最后赠送时间和今日赠送次数
-
FlowerQuotaService.record_quota_usage(quota, amount)
-
-
# 更新接收者的统计
-
update_recipient_statistics(recipient, flower)
-
-
# 发布小红花赠送事件,解耦通知服务
-
DomainEventsService.publish('flower.given', {
-
giver: giver,
-
recipient: recipient,
-
flower: flower,
-
check_in: check_in,
-
amount: amount,
-
comment: comment
-
})
-
-
{
-
success: true,
-
flower: flower,
-
remaining_quota: quota.remaining_flowers,
-
used_today: quota.used_flowers,
-
message: '小红花赠送成功!此操作无法撤回。'
-
}
-
end
-
rescue => e
-
Rails.logger.error "小红花赠送失败: #{e.message}"
-
{
-
success: false,
-
error: '小红花赠送失败,请重试',
-
details: e.message
-
}
-
end
-
-
# 简化的赠送方法(不要求确认)
-
def give_flower_simple(giver, recipient, check_in, amount: 1, comment: nil,
-
flower_type: 'regular', is_anonymous: false)
-
give_flower_with_confirmation(
-
giver, recipient, check_in,
-
amount: amount, comment: comment,
-
flower_type: flower_type, is_anonymous: is_anonymous,
-
confirmed: true
-
)
-
end
-
-
# 批量赠送小红花(管理员功能)
-
def batch_give_flowers(admin_user, flower_data_list)
-
results = []
-
-
ActiveRecord::Base.transaction do
-
flower_data_list.each do |flower_data|
-
result = give_flower_with_confirmation(
-
flower_data[:giver],
-
flower_data[:recipient],
-
flower_data[:check_in],
-
amount: flower_data[:amount] || 1,
-
comment: flower_data[:comment],
-
flower_type: flower_data[:flower_type] || 'regular',
-
is_anonymous: flower_data[:is_anonymous] || false,
-
confirmed: true
-
)
-
results << result
-
end
-
end
-
-
{
-
success: true,
-
total_processed: results.length,
-
successful: results.count { |r| r[:success] },
-
failed: results.count { |r| !r[:success] },
-
details: results
-
}
-
rescue => e
-
Rails.logger.error "批量赠送小红花失败: #{e.message}"
-
{
-
success: false,
-
error: '批量赠送失败',
-
details: e.message
-
}
-
end
-
-
private
-
-
# 发送小红花通知已移至事件订阅者中处理
-
# 这样可以解耦FlowerGivingService和NotificationService的依赖关系
-
-
# 更新接收者的统计信息
-
def update_recipient_statistics(recipient, flower)
-
enrollment = recipient.event_enrollments
-
.where(reading_event_id: flower.check_in.reading_event_id)
-
.first
-
-
if enrollment
-
enrollment.increment!(:flowers_received_count)
-
enrollment.increment!(:total_flowers_received, flower.amount)
-
end
-
end
-
end
-
end
-
# 小红花激励服务 (重构后 - 适配器模式)
-
# 作为统一入口,委托给专门的服务类处理具体业务逻辑
-
# 保持向后兼容性,现有代码无需修改
-
class FlowerIncentiveService
-
class << self
-
# ============================================================================
-
# 配额管理相关方法 - 委托给 FlowerQuotaService
-
# ============================================================================
-
-
# 检查用户是否可以在活动中赠送小红花(每日配额)
-
def can_give_flower?(user, event, amount = 1, date = Date.current)
-
FlowerQuotaService.can_give_flower?(user, event, amount, date)
-
end
-
-
# 获取用户在活动中的每日配额信息
-
def get_user_daily_quota_info(user, event, date = Date.current)
-
FlowerQuotaService.get_user_daily_quota_info(user, event, date)
-
end
-
-
# 获取用户在活动中的配额历史
-
def get_user_quota_history(user, event, days: 7)
-
FlowerQuotaService.get_user_quota_history(user, event, days: days)
-
end
-
-
# 活动开始时初始化所有参与者的每日配额
-
def initialize_event_daily_quotas(event, max_flowers: 3, days: nil)
-
FlowerQuotaService.initialize_event_daily_quotas(event, max_flowers: max_flowers, days: days)
-
end
-
-
# 获取活动的每日配额统计
-
def get_event_daily_quota_stats(event, date = Date.current)
-
FlowerQuotaService.get_event_daily_quota_stats(event, date)
-
end
-
-
# 检查配额是否即将用完(提醒功能)
-
def check_daily_quota_warning(user, event, date = Date.current, threshold: 0.8)
-
FlowerQuotaService.check_daily_quota_warning(user, event, date, threshold: threshold)
-
end
-
-
# 使用配额(扣减数量)
-
def use_quota!(user, event, amount, date = Date.current)
-
FlowerQuotaService.use_quota!(user, event, amount, date)
-
end
-
-
# ============================================================================
-
# 小红花赠送相关方法 - 委托给 FlowerGivingService
-
# ============================================================================
-
-
# 尝试赠送小红花(带每日配额检查和确认提示)
-
def give_flower_with_confirmation(giver, recipient, check_in, amount: 1, comment: nil,
-
flower_type: 'regular', is_anonymous: false, confirmed: false)
-
FlowerGivingService.give_flower_with_confirmation(
-
giver, recipient, check_in,
-
amount: amount, comment: comment,
-
flower_type: flower_type, is_anonymous: is_anonymous,
-
confirmed: confirmed
-
)
-
end
-
-
# 简化的赠送方法(不要求确认)
-
def give_flower_simple(giver, recipient, check_in, amount: 1, comment: nil,
-
flower_type: 'regular', is_anonymous: false)
-
FlowerGivingService.give_flower_simple(
-
giver, recipient, check_in,
-
amount: amount, comment: comment,
-
flower_type: flower_type, is_anonymous: is_anonymous
-
)
-
end
-
-
# 批量赠送小红花(管理员功能)
-
def batch_give_flowers(admin_user, flower_data_list)
-
FlowerGivingService.batch_give_flowers(admin_user, flower_data_list)
-
end
-
-
# ============================================================================
-
# 证书相关方法 - 委托给 FlowerCertificateService
-
# ============================================================================
-
-
# 活动结束时生成小红花证书
-
def finalize_event_flower_certificates(event)
-
FlowerCertificateService.finalize_event_flower_certificates(event)
-
end
-
-
# 获取活动的前三名排行榜
-
def get_event_top_three(event)
-
FlowerCertificateService.get_event_top_three(event)
-
end
-
-
# 获取用户的所有小红花证书
-
def get_user_certificates(user)
-
FlowerCertificateService.get_user_certificates(user)
-
end
-
-
# 验证证书有效性
-
def validate_certificate(certificate_id)
-
FlowerCertificateService.validate_certificate(certificate_id)
-
end
-
-
# 重新生成证书(用于修正错误)
-
def regenerate_certificate(certificate_id, admin_user)
-
FlowerCertificateService.regenerate_certificate(certificate_id, admin_user)
-
end
-
-
# 批量生成参与证书
-
def batch_generate_participation_certificates(event, user_ids = nil)
-
FlowerCertificateService.batch_generate_participation_certificates(event, user_ids)
-
end
-
-
# ============================================================================
-
# 向后兼容性方法 - 保持原有接口不变
-
# ============================================================================
-
-
# 旧版本方法名兼容
-
def can_give_flower_legacy?(user, event, amount = 1)
-
can_give_flower?(user, event, amount, Date.current)
-
end
-
-
def give_flower_with_quota_legacy(giver, recipient, check_in, amount: 1, comment: nil, flower_type: 'regular', is_anonymous: false)
-
give_flower_with_confirmation(giver, recipient, check_in,
-
amount: amount, comment: comment,
-
flower_type: flower_type, is_anonymous: is_anonymous,
-
confirmed: false)
-
end
-
-
def get_user_quota_info_legacy(user, event)
-
get_user_daily_quota_info(user, event, Date.current)
-
end
-
-
def initialize_event_flower_quotas_legacy(event, max_flowers: 3)
-
initialize_event_daily_quotas(event, max_flowers: max_flowers)
-
end
-
-
# 别名方法,确保现有代码继续工作
-
alias_method :can_give_flower_old, :can_give_flower_legacy?
-
alias_method :give_flower_with_quota_old, :give_flower_with_quota_legacy
-
alias_method :get_user_quota_info_old, :get_user_quota_info_legacy
-
alias_method :initialize_event_flower_quotas_old, :initialize_event_flower_quotas_legacy
-
-
# ============================================================================
-
# 便捷方法和组合操作
-
# ============================================================================
-
-
# 一键检查用户在活动中的完整状态
-
def get_user_complete_status(user, event, date = Date.current)
-
{
-
quota_info: get_user_daily_quota_info(user, event, date),
-
can_give_flower: can_give_flower?(user, event, 1, date),
-
quota_warning: check_daily_quota_warning(user, event, date, threshold: 0.8),
-
certificates: get_user_certificates(user)
-
}
-
end
-
-
# 获取活动完整统计信息
-
def get_event_complete_stats(event, date = Date.current)
-
{
-
quota_stats: get_event_daily_quota_stats(event, date),
-
top_three: event.completed? ? get_event_top_three(event) : nil,
-
event_status: {
-
status: event.status,
-
participants_count: event.participants.count,
-
is_completed: event.completed?
-
}
-
}
-
end
-
-
# 智能赠送建议(基于配额和历史数据)
-
def get_smart_giving_suggestions(giver, event, limit = 5)
-
quota_info = get_user_daily_quota_info(giver, event)
-
return { suggestions: [], message: '今日配额已用完' } unless quota_info[:can_give_more]
-
-
# 获取今日可赠送的打卡列表
-
available_check_ins = CheckIn.joins(:reading_schedule, :user)
-
.where(reading_schedules: { reading_event: event, date: Date.current })
-
.where.not(user: giver)
-
.includes(:user)
-
.limit(limit)
-
-
suggestions = available_check_ins.map do |check_in|
-
{
-
check_in: check_in.as_json_for_api(include_user: true),
-
recommended_flower_type: 'regular',
-
reason: '今日打卡,值得鼓励'
-
}
-
end
-
-
{
-
suggestions: suggestions,
-
remaining_quota: quota_info[:remaining_flowers],
-
message: "发现 #{suggestions.count} 个可赠送的打卡"
-
}
-
end
-
end
-
end
-
# 小红花配额管理服务
-
# 负责管理用户的每日小红花配额
-
class FlowerQuotaService
-
class << self
-
# 检查用户是否可以在活动中赠送小红花(每日配额)
-
def can_give_flower?(user, event, amount = 1, date = Date.current)
-
return false unless user && event
-
return false unless event.participants.include?(user)
-
return false unless event.status.in?(['in_progress', 'approved'])
-
-
# 检查是否是活动日
-
return false unless activity_day?(event, date)
-
-
quota = get_daily_quota(user, event, date)
-
quota.can_give_flower?(amount)
-
end
-
-
# 获取用户在活动中的每日配额信息
-
def get_user_daily_quota_info(user, event, date = Date.current)
-
return { error: '用户或活动不存在' } unless user && event
-
-
quota = get_daily_quota(user, event, date)
-
-
{
-
user_id: user.id,
-
event_id: event.id,
-
date: date,
-
is_activity_day: activity_day?(event, date),
-
used_flowers: quota.used_flowers,
-
max_flowers: quota.max_flowers,
-
remaining_flowers: quota.remaining_flowers,
-
usage_percentage: quota.usage_percentage,
-
can_give_more: quota.can_give_flower?(1),
-
last_given_at: quota.last_given_at,
-
give_count_today: quota.give_count_today,
-
time_remaining: time_remaining_for_quota(date)
-
}
-
end
-
-
# 获取用户在活动中的配额历史
-
def get_user_quota_history(user, event, days: 7)
-
return { error: '用户或活动不存在' } unless user && event
-
-
end_date = Date.current
-
start_date = end_date - days.days + 1
-
-
quotas = []
-
(start_date..end_date).each do |date|
-
quota_info = get_user_daily_quota_info(user, event, date)
-
quotas << quota_info
-
end
-
-
{
-
user: user.as_json_for_api,
-
event: event.as_json_for_api,
-
period: "#{start_date} 至 #{end_date}",
-
quotas: quotas
-
}
-
end
-
-
# 活动开始时初始化所有参与者的每日配额
-
def initialize_event_daily_quotas(event, max_flowers: 3, days: nil)
-
return false unless event
-
-
# 默认初始化活动期间的所有日期
-
days ||= event.days_count
-
-
event.participants.each do |user|
-
event.start_date.upto(event.end_date) do |date|
-
next if event.weekend_rest && (date.saturday? || date.sunday?)
-
-
get_daily_quota(user, event, date, max_flowers)
-
end
-
end
-
-
true
-
end
-
-
# 获取活动的每日配额统计
-
def get_event_daily_quota_stats(event, date = Date.current)
-
return { error: '活动不存在' } unless event
-
-
# 获取当日的所有配额记录
-
quotas = FlowerQuota.joins(:user)
-
.where(reading_event: event, quota_date: date)
-
.includes(:user)
-
-
total_users = quotas.count
-
total_used = quotas.sum(:used_flowers)
-
total_max = quotas.sum(:max_flowers)
-
users_with_remaining = quotas.select { |q| q.can_give_flower?(1) }.count
-
users_exhausted = quotas.select { |q| q.remaining_flowers == 0 }.count
-
-
{
-
event: event.as_json_for_api,
-
date: date,
-
is_activity_day: activity_day?(event, date),
-
statistics: {
-
total_users: total_users,
-
total_used: total_used,
-
total_max: total_max,
-
overall_usage_rate: total_max > 0 ? (total_used.to_f / total_max * 100).round(2) : 0,
-
users_with_remaining: users_with_remaining,
-
users_exhausted: users_exhausted
-
},
-
top_givers: quotas.order(give_count_today: :desc).limit(10).map do |quota|
-
{
-
user: quota.user.as_json_for_api,
-
used_flowers: quota.used_flowers,
-
give_count_today: quota.give_count_today,
-
last_given_at: quota.last_given_at
-
}
-
end
-
}
-
end
-
-
# 检查配额是否即将用完(提醒功能)
-
def check_daily_quota_warning(user, event, date = Date.current, threshold: 0.8)
-
return { should_warn: false } unless activity_day?(event, date)
-
-
quota = get_daily_quota(user, event, date)
-
return { should_warn: false } unless quota.max_flowers > 0
-
-
usage_ratio = quota.used_flowers.to_f / quota.max_flowers
-
-
if usage_ratio >= threshold
-
{
-
should_warn: true,
-
remaining_flowers: quota.remaining_flowers,
-
usage_percentage: quota.usage_percentage,
-
message: "今日小红花配额即将用完,还剩余 #{quota.remaining_flowers} 朵",
-
time_remaining: time_remaining_for_quota(date)
-
}
-
else
-
{
-
should_warn: false,
-
remaining_flowers: quota.remaining_flowers,
-
usage_percentage: quota.usage_percentage,
-
time_remaining: time_remaining_for_quota(date)
-
}
-
end
-
end
-
-
# 使用配额(扣减数量)
-
def use_quota!(user, event, amount, date = Date.current)
-
quota = get_daily_quota(user, event, date)
-
quota.use_flowers!(amount)
-
end
-
-
# 增加配额使用记录
-
def record_quota_usage(quota, amount)
-
quota.update!(
-
last_given_at: Time.current,
-
give_count_today: quota.give_count_today + amount
-
)
-
end
-
-
private
-
-
# 获取用户在指定日期的配额
-
def get_daily_quota(user, event, date = Date.current, max_flowers = 3)
-
FlowerQuota.find_or_initialize_by(
-
user: user,
-
reading_event: event,
-
quota_date: date
-
).tap do |quota|
-
if quota.new_record?
-
quota.max_flowers = max_flowers
-
quota.used_flowers = 0
-
quota.give_count_today = 0
-
quota.save!
-
end
-
end
-
end
-
-
# 检查指定日期是否是活动阅读日
-
def activity_day?(event, date)
-
return false unless event.start_date && event.end_date
-
return false if date < event.start_date || date > event.end_date
-
-
# 如果设置周末休息,跳过周末
-
if event.weekend_rest && (date.saturday? || date.sunday?)
-
return false
-
end
-
-
true
-
end
-
-
# 计算配额剩余时间
-
def time_remaining_for_quota(date)
-
return "已过期" if date < Date.current
-
return "全天可用" if date > Date.current
-
-
# 如果是今天,计算到23:59:59的剩余时间
-
if date == Date.current
-
end_of_day = Time.current.end_of_day
-
remaining_seconds = end_of_day - Time.current
-
remaining_hours = remaining_seconds / 1.hour
-
"#{remaining_hours.round(1)} 小时"
-
else
-
"全天可用"
-
end
-
end
-
end
-
end
-
# 小红花统计服务
-
# 提供小红花统计、排行榜、数据分析功能
-
class FlowerStatisticsService
-
class << self
-
# 获取用户小红花统计
-
def get_user_flower_stats(user, days = 30)
-
start_date = days.days.ago.to_date
-
-
received = Flower.joins(:check_in)
-
.where(recipient: user)
-
.where('flowers.created_at >= ?', start_date)
-
.group(:flower_type)
-
.sum(:amount)
-
-
given = Flower.where(giver: user)
-
.where('flowers.created_at >= ?', start_date)
-
.group(:flower_type)
-
.sum(:amount)
-
-
# 按天统计
-
daily_received = Flower.joins(:check_in)
-
.where(recipient: user)
-
.where('flowers.created_at >= ?', start_date)
-
.group('DATE(flowers.created_at)')
-
.sum(:amount)
-
-
daily_given = Flower.where(giver: user)
-
.where('flowers.created_at >= ?', start_date)
-
.group('DATE(flowers.created_at)')
-
.sum(:amount)
-
-
{
-
period: "#{days}天",
-
total_received: received.values.sum,
-
total_given: given.values.sum,
-
received_by_type: received,
-
given_by_type: given,
-
daily_received: daily_received,
-
daily_given: daily_given,
-
net_balance: received.values.sum - given.values.sum
-
}
-
end
-
-
# 获取活动小红花统计
-
def get_event_flower_stats(event, days = 30)
-
start_date = days.days.ago.to_date
-
-
flowers = Flower.joins(:check_in)
-
.joins(:reading_schedule)
-
.where(reading_schedules: { reading_event_id: event.id })
-
.where('flowers.created_at >= ?', start_date)
-
-
# 按天统计
-
daily_stats = flowers.group('DATE(flowers.created_at)').count
-
-
# 按类型统计
-
type_stats = flowers.group(:flower_type).count
-
-
# 参与度统计
-
participant_stats = flowers.group(:recipient_id).count
-
-
# 发放者统计
-
giver_stats = flowers.group(:giver_id).count
-
-
{
-
period: "#{days}天",
-
total_flowers: flowers.count,
-
daily_stats: daily_stats,
-
type_stats: type_stats,
-
participant_count: participant_stats.keys.count,
-
giver_count: giver_stats.keys.count,
-
avg_flowers_per_participant: participant_stats.values.empty? ? 0 : (participant_stats.values.sum.to_f / participant_stats.count).round(2)
-
}
-
end
-
-
# 获取小红花排行榜
-
def get_flower_leaderboard(type = 'received', period = 30, limit = 20)
-
start_date = period.days.ago.to_date
-
-
case type.to_sym
-
when :received
-
get_received_leaderboard(start_date, limit)
-
when :given
-
get_given_leaderboard(start_date, limit)
-
when :popular_check_ins
-
get_popular_check_ins_leaderboard(start_date, limit)
-
when :generous_givers
-
get_generous_givers_leaderboard(start_date, limit)
-
else
-
get_received_leaderboard(start_date, limit)
-
end
-
end
-
-
# 获取小红花趋势数据
-
def get_flower_trends(days = 30)
-
start_date = days.days.ago.to_date
-
end_date = Date.current
-
-
trends = {}
-
(start_date..end_date).each do |date|
-
flowers = Flower.where('DATE(flowers.created_at) = ?', date)
-
trends[date.to_s] = {
-
total: flowers.count,
-
received: flowers.where.not(giver_id: nil).count,
-
given: flowers.where.not(recipient_id: nil).count
-
}
-
end
-
-
trends
-
end
-
-
# 获取小红花激励统计
-
def get_incentive_statistics(days = 30)
-
start_date = days.days.ago.to_date
-
end_date = Date.current
-
-
# 小红花发放活动参与度
-
active_events = ReadingEvent.joins(check_ins: :flowers)
-
.where('reading_events.start_date <= ? AND reading_events.end_date >= ?', end_date, start_date)
-
.where('flowers.created_at >= ?', start_date)
-
.distinct
-
.count
-
-
# 活跃用户(发送或接收小红花)
-
active_users = Flower.where('flowers.created_at >= ?', start_date)
-
.select('DISTINCT giver_id, recipient_id')
-
.flat_map { |r| [r.giver_id, r.recipient_id] }
-
.uniq
-
.count
-
-
# 小红花流转情况
-
total_flowers = Flower.where('flowers.created_at >= ?', start_date).count
-
avg_flowers_per_day = total_flowers.to_f / days
-
-
{
-
period: "#{days}天",
-
active_events: active_events,
-
active_users: active_users,
-
total_flowers: total_flowers,
-
avg_flowers_per_day: avg_flowers_per_day.round(2),
-
flower_velocity: calculate_flower_velocity(start_date, days)
-
}
-
end
-
-
# 获取小红花发放建议
-
def get_flower_suggestions(user, limit = 5)
-
# 建议给哪些打卡送小红花
-
suggestions = []
-
-
# 1. 最近的高质量打卡但小红花较少的内容
-
recent_check_ins = CheckIn.joins(:flowers)
-
.where.not(check_ins: { user_id: user.id })
-
.where('check_ins.created_at >= ?', 7.days.ago)
-
.where('check_ins.word_count >= 100')
-
.group('check_ins.id')
-
.having('COUNT(flowers.id) < 3')
-
.order('check_ins.created_at DESC')
-
.limit(limit)
-
-
recent_check_ins.each do |check_in|
-
suggestions << {
-
type: 'check_in',
-
check_in: check_in,
-
reason: '高质量内容但小红花较少',
-
priority: 1
-
}
-
end
-
-
# 2. 活跃的参与者 - 简化版本,只考虑最近打卡的用户
-
active_participants = User.joins(:check_ins)
-
.where('check_ins.created_at >= ?', 7.days.ago)
-
.where.not(users: { id: user.id })
-
.group('users.id')
-
.having('COUNT(check_ins.id) >= 3')
-
.select('users.*')
-
.order('COUNT(check_ins.id) DESC')
-
.limit(limit)
-
-
active_participants.each do |participant|
-
suggestions << {
-
type: 'user',
-
user: participant,
-
reason: '活跃参与者',
-
priority: 2
-
}
-
end
-
-
suggestions.sort_by { |s| s[:priority] }.first(limit)
-
end
-
-
# 计算小红花流速
-
def calculate_flower_velocity(start_date, days)
-
flowers = Flower.where('flowers.created_at >= ?', start_date)
-
.order(:created_at)
-
-
return 0.0 if flowers.count < 2
-
-
first_flower = flowers.first
-
last_flower = flowers.last
-
-
time_span = (last_flower.created_at - first_flower.created_at) / 1.hour
-
return 0.0 if time_span < 1
-
-
(flowers.count - 1).to_f / time_span.round(2)
-
end
-
-
private
-
-
# 获取接收排行榜
-
def get_received_leaderboard(start_date, limit)
-
flower_sums = Flower.where('flowers.created_at >= ?', start_date)
-
.joins(:recipient)
-
.group(:recipient_id)
-
.sum(:amount)
-
-
user_ids = flower_sums.keys.sort_by { |user_id| -flower_sums[user_id] }.first(limit)
-
users = User.where(id: user_ids).index_by(&:id)
-
-
user_ids.map do |user_id|
-
user = users[user_id]
-
next unless user
-
-
# 添加total_flowers属性
-
user.define_singleton_method(:total_flowers) { flower_sums[user_id] }
-
user
-
end.compact
-
end
-
-
# 获取赠送排行榜
-
def get_given_leaderboard(start_date, limit)
-
flower_sums = Flower.where('flowers.created_at >= ?', start_date)
-
.joins(:giver)
-
.group(:giver_id)
-
.sum(:amount)
-
-
user_ids = flower_sums.keys.sort_by { |user_id| -flower_sums[user_id] }.first(limit)
-
users = User.where(id: user_ids).index_by(&:id)
-
-
user_ids.map do |user_id|
-
user = users[user_id]
-
next unless user
-
-
# 添加total_flowers属性
-
user.define_singleton_method(:total_flowers) { flower_sums[user_id] }
-
user
-
end.compact
-
end
-
-
# 获取热门打卡排行榜
-
def get_popular_check_ins_leaderboard(start_date, limit)
-
flower_counts = Flower.where('flowers.created_at >= ?', start_date)
-
.group(:check_in_id)
-
.count
-
-
check_in_ids = flower_counts.keys.sort_by { |check_in_id| -flower_counts[check_in_id] }.first(limit)
-
check_ins = CheckIn.where(id: check_in_ids).includes(:user).index_by(&:id)
-
-
check_in_ids.map do |check_in_id|
-
check_in = check_ins[check_in_id]
-
next unless check_in
-
-
# 添加flower_count属性
-
check_in.define_singleton_method(:flower_count) { flower_counts[check_in_id] }
-
check_in
-
end.compact
-
end
-
-
# 获取慷慨赠送者排行榜
-
def get_generous_givers_leaderboard(start_date, limit)
-
flower_counts = Flower.where('flowers.created_at >= ?', start_date)
-
.where.not(is_anonymous: true)
-
.group(:giver_id)
-
.count
-
-
user_ids = flower_counts.keys.sort_by { |user_id| -flower_counts[user_id] }.first(limit)
-
users = User.where(id: user_ids).index_by(&:id)
-
-
user_ids.map do |user_id|
-
user = users[user_id]
-
next unless user
-
-
# 添加giving_count属性
-
user.define_singleton_method(:giving_count) { flower_counts[user_id] }
-
user
-
end.compact
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# GlobalErrorHandlerService - 全局错误处理服务
-
# 提供统一的错误处理、日志记录和用户友好的错误响应
-
class GlobalErrorHandlerService < ApplicationService
-
include ServiceInterface
-
-
attr_reader :exception, :context, :user, :request_id
-
-
def initialize(exception:, context: {}, user: nil, request_id: nil)
-
super()
-
@exception = exception
-
@context = context
-
@user = user
-
@request_id = request_id || SecureRandom.uuid
-
end
-
-
def call
-
handle_errors do
-
log_error
-
determine_error_response
-
end
-
self
-
end
-
-
# 类方法:处理控制器异常
-
def self.handle_controller_exception(exception, controller, action = nil)
-
user = controller.respond_to?(:current_user) ? controller.current_user : nil
-
request_id = controller.request&.request_id
-
-
new(
-
exception: exception,
-
context: {
-
controller: controller.class.name,
-
action: action,
-
method: controller.request&.request_method,
-
path: controller.request&.path,
-
ip: controller.request&.remote_ip,
-
user_agent: controller.request&.user_agent
-
},
-
user: user,
-
request_id: request_id
-
).call
-
end
-
-
# 类方法:处理服务异常
-
def self.handle_service_exception(exception, service_name, action = nil)
-
new(
-
exception: exception,
-
context: {
-
service: service_name,
-
action: action
-
},
-
user: nil,
-
request_id: SecureRandom.uuid
-
).call
-
end
-
-
def error_response
-
@error_response
-
end
-
-
def error_code
-
@error_code ||= determine_error_code
-
end
-
-
def error_message
-
@error_message ||= determine_error_message
-
end
-
-
def should_retry?
-
@should_retry ||= determine_retry_eligibility
-
end
-
-
def severity
-
@severity ||= determine_severity
-
end
-
-
private
-
-
def log_error
-
return unless should_log_error?
-
-
# 基本错误信息
-
Rails.logger.error(
-
"[#{severity.upcase}] #{exception.class.name}: #{exception.message}",
-
{
-
request_id: request_id,
-
user_id: user&.id,
-
user_role: user&.role_as_string,
-
context: context,
-
exception_class: exception.class.name,
-
exception_message: exception.message,
-
backtrace: exception.backtrace&.first(10)
-
}
-
)
-
-
# 详细错误信息(仅在开发环境)
-
if Rails.env.development?
-
Rails.logger.debug(
-
"完整错误堆栈:",
-
exception.backtrace&.join("\n")
-
)
-
end
-
-
# 发送错误通知(生产环境)
-
send_error_notification if should_send_notification?
-
end
-
-
def determine_error_response
-
case exception
-
when ActionController::ParameterMissing, ActionController::BadRequest
-
build_validation_error_response
-
when ActiveRecord::RecordNotFound
-
build_not_found_error_response
-
when ActiveRecord::RecordInvalid
-
build_validation_error_response
-
when ActionDispatch::Http::Parameters::InvalidParameter
-
build_parameter_error_response
-
when JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature
-
build_authentication_error_response
-
when Timeout::Error, ActiveRecord::StatementInvalid
-
build_system_error_response
-
else
-
build_general_error_response
-
end
-
end
-
-
def determine_error_code
-
case exception
-
when ActionController::ParameterMissing
-
'MISSING_PARAMETER'
-
when ActionController::BadRequest
-
'INVALID_REQUEST'
-
when ActiveRecord::RecordNotFound
-
'RESOURCE_NOT_FOUND'
-
when ActiveRecord::RecordInvalid
-
'VALIDATION_ERROR'
-
when ActionDispatch::Http::Parameters::InvalidParameter
-
'INVALID_PARAMETER'
-
when JWT::DecodeError, JWT::VerificationError
-
'INVALID_TOKEN'
-
when JWT::ExpiredSignature
-
'TOKEN_EXPIRED'
-
when Timeout::Error
-
'TIMEOUT_ERROR'
-
when ActiveRecord::StatementInvalid
-
'DATABASE_ERROR'
-
else
-
'INTERNAL_ERROR'
-
end
-
end
-
-
def determine_error_message
-
case exception
-
when ActionController::ParameterMissing
-
"缺少必需的参数: #{exception.param}"
-
when ActionController::BadRequest
-
"请求格式错误"
-
when ActiveRecord::RecordNotFound
-
"请求的资源不存在"
-
when ActiveRecord::RecordInvalid
-
"数据验证失败: #{format_validation_errors}"
-
when ActionDispatch::Http::Parameters::InvalidParameter
-
"参数格式错误: #{exception.message}"
-
when JWT::DecodeError, JWT::VerificationError
-
"认证令牌无效"
-
when JWT::ExpiredSignature
-
"认证令牌已过期"
-
when Timeout::Error
-
"请求超时,请稍后重试"
-
when ActiveRecord::StatementInvalid
-
"数据库操作失败"
-
else
-
"系统繁忙,请稍后重试"
-
end
-
end
-
-
def determine_retry_eligibility
-
# 可以重试的错误类型
-
retryable_errors = [
-
Timeout::Error,
-
ActiveRecord::StatementInvalid,
-
Net::TimeoutError,
-
Net::ReadTimeout,
-
Net::OpenTimeout
-
]
-
-
retryable_errors.include?(exception.class) && !should_fail_fast?
-
end
-
-
def determine_severity
-
case exception
-
when ActionController::ParameterMissing, ActiveRecord::RecordInvalid
-
:low
-
when ActionDispatch::Http::Parameters::InvalidParameter, JWT::DecodeError
-
:medium
-
when Timeout::Error, ActiveRecord::StatementInvalid
-
:high
-
else
-
:critical
-
end
-
end
-
-
def build_validation_error_response
-
errors = extract_validation_errors
-
-
{
-
error: error_message,
-
error_code: error_code,
-
error_type: 'validation_error',
-
errors: errors,
-
request_id: request_id,
-
timestamp: Time.current.iso8601,
-
details: {
-
context: context,
-
fix_suggestions: generate_fix_suggestions
-
}
-
}
-
end
-
-
def build_not_found_error_response
-
{
-
error: error_message,
-
error_code: error_code,
-
error_type: 'not_found',
-
request_id: request_id,
-
timestamp: Time.current.iso8601,
-
details: {
-
context: context,
-
suggestions: [
-
'请检查资源ID是否正确',
-
'确认资源是否存在且未被删除'
-
]
-
}
-
}
-
end
-
-
def build_parameter_error_response
-
{
-
error: error_message,
-
error_code: error_code,
-
error_type: 'parameter_error',
-
request_id: request_id,
-
timestamp: Time.current.iso8601,
-
details: {
-
context: context,
-
parameter_info: extract_parameter_info,
-
suggestions: [
-
'请检查请求参数格式',
-
'参考API文档确认参数要求'
-
]
-
}
-
}
-
end
-
-
def build_authentication_error_response
-
{
-
error: error_message,
-
error_code: error_code,
-
error_type: 'authentication_error',
-
request_id: request_id,
-
timestamp: Time.current.iso8601,
-
details: {
-
context: context,
-
auth_info: extract_auth_info,
-
suggestions: [
-
'请重新登录获取有效令牌',
-
'检查令牌是否完整且未过期'
-
]
-
}
-
}
-
end
-
-
def build_system_error_response
-
{
-
error: error_message,
-
error_code: error_code,
-
error_type: 'system_error',
-
request_id: request_id,
-
timestamp: Time.current.iso8601,
-
details: {
-
context: context,
-
severity: severity,
-
suggestions: [
-
'请稍后重试',
-
'如问题持续存在,请联系技术支持'
-
]
-
}
-
}
-
end
-
-
def build_general_error_response
-
{
-
error: error_message,
-
error_code: error_code,
-
error_type: 'general_error',
-
request_id: request_id,
-
timestamp: Time.current.iso8601,
-
details: {
-
context: context,
-
exception_class: exception.class.name,
-
severity: severity,
-
suggestions: [
-
'请检查请求格式并重试',
-
'如问题持续存在,请联系技术支持'
-
]
-
}
-
}
-
end
-
-
def extract_validation_errors
-
if exception.is_a?(ActiveRecord::RecordInvalid)
-
exception.record.errors.full_messages
-
elsif exception.is_a?(ActionController::ParameterMissing)
-
[exception.message]
-
elsif exception.respond_to?(:errors)
-
exception.errors.full_messages
-
else
-
[exception.message]
-
end
-
end
-
-
def extract_parameter_info
-
return {} unless context
-
-
{
-
method: context[:method],
-
path: context[:path],
-
controller: context[:controller],
-
action: context[:action]
-
}
-
end
-
-
def extract_auth_info
-
return {} unless user
-
-
{
-
user_id: user.id,
-
user_role: user.role_as_string,
-
user_nickname: user.nickname
-
}
-
end
-
-
def generate_fix_suggestions
-
suggestions = []
-
-
case exception
-
when ActiveRecord::RecordInvalid
-
suggestions << "请检查必填字段是否完整"
-
suggestions << "确认数据格式是否正确"
-
when ActionController::ParameterMissing
-
suggestions << "请添加缺少的必需参数"
-
when ActionController::BadRequest
-
suggestions << "请检查请求格式和参数"
-
end
-
-
suggestions
-
end
-
-
def should_log_error?
-
# 不记录的错误类型(避免日志噪音)
-
non_loggable_errors = [
-
'MISSING_PARAMETER',
-
'INVALID_REQUEST'
-
]
-
-
return false if non_loggable_errors.include?(error_code)
-
true
-
end
-
-
def should_send_notification?
-
return false unless Rails.env.production?
-
return false unless severity == :critical
-
return false if exception.is_a?(ActiveRecord::RecordNotFound)
-
return false if exception.is_a?(ActionController::ParameterMissing)
-
-
true
-
end
-
-
def send_error_notification
-
# 这里可以集成通知系统,如Slack、邮件等
-
# 示例实现:
-
begin
-
ErrorNotificationService.notify(
-
error: exception,
-
context: context,
-
user: user,
-
request_id: request_id
-
)
-
rescue => e
-
Rails.logger.error "发送错误通知失败: #{e.message}"
-
end
-
end
-
-
def should_fail_fast?
-
# 需要快速失败的错误类型
-
fail_fast_errors = [
-
'MISSING_AUTH_HEADER',
-
'INVALID_TOKEN_FORMAT',
-
'TOKEN_EXPIRED'
-
]
-
-
fail_fast_errors.include?(error_code)
-
end
-
end
-
# frozen_string_literal: true
-
-
# LeaderAssignmentService - 领读人分配管理服务
-
# 负责多种分配算法、权限管理、工作统计、补位机制等业务逻辑
-
1
class LeaderAssignmentService < ApplicationService
-
1
attr_reader :event, :user, :schedule, :action, :assignment_options
-
-
1
def initialize(event:, user: nil, schedule: nil, action: nil, assignment_options: {})
-
3
super()
-
3
@event = event
-
3
@user = user
-
3
@schedule = schedule
-
3
@action = action
-
3
@assignment_options = assignment_options.with_indifferent_access
-
end
-
-
# 主要调用方法
-
1
def call
-
3
handle_errors do
-
3
case action
-
when :claim_leadership
-
claim_leadership
-
when :auto_assign
-
3
auto_assign_leaders
-
when :backup_assign
-
backup_assignment
-
when :reassign
-
reassign_leader
-
when :get_statistics
-
get_assignment_statistics
-
when :check_permissions
-
check_leader_permissions
-
else
-
failure!("不支持的操作: #{action}")
-
end
-
end
-
end
-
-
# 类方法:自由报名领读
-
1
def self.claim_leadership!(event, user, schedule)
-
new(event: event, user: user, schedule: schedule, action: :claim_leadership).call
-
end
-
-
# 类方法:自动分配领读人
-
1
def self.auto_assign_leaders!(event, assignment_type: nil, options: {})
-
3
new(event: event, action: :auto_assign, assignment_options: { assignment_type: assignment_type }.merge(options)).call
-
end
-
-
# 类方法:补位分配
-
1
def self.backup_assignment!(event, schedule, backup_leader)
-
new(event: event, schedule: schedule, user: backup_leader, action: :backup_assign).call
-
end
-
-
# 类方法:重新分配领读人
-
1
def self.reassign_leader!(event, schedule, new_leader)
-
new(event: event, schedule: schedule, user: new_leader, action: :reassign).call
-
end
-
-
# 类方法:获取分配统计
-
1
def self.assignment_statistics(event)
-
new(event: event, action: :get_statistics).call
-
end
-
-
# 类方法:检查领读权限
-
1
def self.check_permissions(event, user, schedule = nil)
-
new(event: event, user: user, schedule: schedule, action: :check_permissions).call
-
end
-
-
1
private
-
-
# 自由报名领读
-
1
def claim_leadership
-
# 检查是否是自由报名模式
-
unless event.leader_assignment_type == 'voluntary'
-
return failure!("该活动不支持自由报名领读")
-
end
-
-
# 检查是否已报名该活动
-
unless user.enrollments.exists?(reading_event: event)
-
return failure!("请先报名该活动")
-
end
-
-
# 检查是否已有领读人
-
if schedule.daily_leader.present?
-
return failure!("该日已有领读人")
-
end
-
-
# 检查领读次数限制
-
leadership_count = event.reading_schedules.where(daily_leader: user).count
-
if leadership_count >= 3
-
return failure!("领读次数已达上限")
-
end
-
-
# 分配领读人
-
schedule.update!(daily_leader: user)
-
-
success!({
-
message: "领读报名成功",
-
schedule_data: {
-
id: schedule.id,
-
day_number: schedule.day_number,
-
date: schedule.date,
-
leader: {
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
}
-
}
-
})
-
end
-
-
# 自动分配领读人(支持多种算法)
-
1
def auto_assign_leaders
-
3
return failure!("活动未审批或没有日程安排") unless event.approved? && event.reading_schedules.any?
-
-
3
assignment_type = @assignment_options[:assignment_type] || event.leader_assignment_type
-
-
3
case assignment_type.to_sym
-
when :random
-
1
assign_random_leaders!
-
when :balanced
-
assign_balanced_leaders!
-
when :rotation
-
assign_rotation_leaders!
-
when :voluntary
-
2
assign_voluntary_leaders!
-
else
-
return failure!("不支持的分配方式: #{assignment_type}")
-
end
-
-
2
success!({
-
message: "领读人分配完成",
-
assignment_type: assignment_type,
-
assigned_count: event.reading_schedules.where.not(daily_leader: nil).count
-
})
-
end
-
-
# 随机分配领读人算法
-
1
def assign_random_leaders!
-
1
participants = get_available_participants
-
return failure!("没有参与者可供分配") if participants.empty?
-
-
schedules = event.reading_schedules.order(:day_number)
-
schedules.each_with_index do |schedule, index|
-
leader = participants[index % participants.length]
-
schedule.update!(daily_leader: leader)
-
end
-
end
-
-
# 平衡分配算法(基于历史工作量)
-
1
def assign_balanced_leaders!
-
participants = get_available_participants
-
return failure!("没有参与者可供分配") if participants.empty?
-
-
# 计算历史工作量
-
leader_workloads = calculate_leader_workloads(participants)
-
-
schedules = event.reading_schedules.order(:day_number)
-
schedules.each do |schedule|
-
# 选择工作量最小的参与者
-
least_busy_leader = leader_workloads.min_by { |_, workload| workload }.first
-
schedule.update!(daily_leader: least_busy_leader)
-
-
# 更新工作量
-
leader_workloads[least_busy_leader] += 1
-
end
-
end
-
-
# 轮换分配算法(确保每个人都能领读,避免连续领读)
-
1
def assign_rotation_leaders!
-
participants = get_available_participants
-
return failure!("没有参与者可供分配") if participants.empty?
-
-
schedules = event.reading_schedules.order(:day_number)
-
rotation_queue = participants.dup
-
last_leader = nil
-
-
schedules.each do |schedule|
-
# 避免连续分配给同一个人
-
if rotation_queue.first == last_leader && rotation_queue.size > 1
-
rotation_queue.rotate!
-
end
-
-
leader = rotation_queue.first
-
schedule.update!(daily_leader: leader)
-
last_leader = leader
-
-
# 将领过的人移到队列末尾
-
rotation_queue.rotate!
-
end
-
end
-
-
# 自愿分配算法(基于自愿报名)
-
1
def assign_voluntary_leaders!
-
2
volunteer_assignments = @assignment_options[:volunteer_assignments] || {}
-
2
schedules = event.reading_schedules.order(:day_number)
-
-
2
assigned_count = 0
-
2
schedules.each do |schedule|
-
4
if volunteer_assignments[schedule.id]
-
user_id = volunteer_assignments[schedule.id]
-
user = User.find_by(id: user_id)
-
-
if user && can_be_leader?(user)
-
schedule.update!(daily_leader: user)
-
assigned_count += 1
-
end
-
end
-
end
-
-
2
success!({
-
message: "自愿分配完成",
-
assigned_count: assigned_count
-
})
-
end
-
-
# 补位分配机制
-
1
def backup_assignment
-
return failure!("补位需要指定日程和补位人") unless schedule && user
-
-
# 检查补位权限
-
unless event.leader == user
-
return failure!("只有活动创建者可以进行补位分配")
-
end
-
-
# 检查日程是否需要补位
-
unless schedule_needs_backup?(schedule)
-
return failure!("该日程不需要补位")
-
end
-
-
ActiveRecord::Base.transaction do
-
schedule.update!(daily_leader: user)
-
-
# 记录补位操作
-
log_backup_assignment(schedule, user)
-
end
-
-
success!({
-
message: "补位分配成功",
-
schedule: schedule_info(schedule),
-
backup_leader: user_info(user)
-
})
-
end
-
-
# 重新分配领读人
-
1
def reassign_leader
-
return failure!("需要指定日程和新领读人") unless schedule && user
-
-
# 检查权限
-
unless can_reassign_leader?(user)
-
return failure!("权限不足")
-
end
-
-
old_leader = schedule.daily_leader
-
ActiveRecord::Base.transaction do
-
schedule.update!(daily_leader: user)
-
-
# 记录重新分配操作
-
log_reassignment(schedule, old_leader, user)
-
end
-
-
success!({
-
message: "领读人重新分配成功",
-
schedule: schedule_info(schedule),
-
old_leader: old_leader ? user_info(old_leader) : nil,
-
new_leader: user_info(user)
-
})
-
end
-
-
# 获取分配统计信息
-
1
def get_assignment_statistics
-
schedules = event.reading_schedules.includes(:daily_leader, :daily_leading)
-
-
total_schedules = schedules.count
-
assigned_schedules = schedules.where.not(daily_leader: nil).count
-
leaders = schedules.where.not(daily_leader: nil).pluck(:daily_leader_id).uniq
-
-
stats = {
-
total_schedules: total_schedules,
-
assigned_schedules: assigned_schedules,
-
unassigned_schedules: total_schedules - assigned_schedules,
-
unique_leaders: leaders.count,
-
assignment_rate: total_schedules > 0 ? (assigned_schedules.to_f / total_schedules * 100).round(2) : 0,
-
leader_workload: calculate_leader_workload_statistics(schedules),
-
backup_needed: backup_needed_schedules.size,
-
content_completion_rate: calculate_content_completion_rate(schedules)
-
}
-
-
success!(stats)
-
end
-
-
# 检查领读权限
-
1
def check_leader_permissions
-
return success!({ can_view: false, message: "用户不存在" }) unless user
-
return success!({ can_view: false, message: "用户未报名活动" }) unless user.enrolled?(event)
-
-
permissions = {
-
can_view: true,
-
can_claim_leadership: can_claim_leadership?,
-
can_be_assigned: can_be_assigned_as_leader?,
-
can_backup: can_backup_assignment?,
-
current_schedules: user_leading_schedules,
-
permission_window: get_permission_window_info
-
}
-
-
success!(permissions)
-
end
-
-
1
private
-
-
# 辅助方法
-
1
def get_available_participants
-
1
event.enrollments.includes(:user).where(role: :participant).map(&:user).compact
-
end
-
-
1
def can_be_leader?(user)
-
return false unless user
-
return false unless user.enrolled?(event)
-
return false unless event.enrollments.find_by(user: user)&.participant?
-
-
true
-
end
-
-
1
def can_claim_leadership?
-
return false unless event.leader_assignment_type == 'voluntary'
-
return false unless schedule
-
return false if schedule.daily_leader.present?
-
-
# 检查领读次数限制
-
leadership_count = event.reading_schedules.where(daily_leader: user).count
-
leadership_count < (@assignment_options[:max_leadership_count] || 3)
-
end
-
-
1
def can_be_assigned_as_leader?
-
can_be_leader?(user) && event.in_progress?
-
end
-
-
1
def can_backup_assignment?
-
event.leader == user
-
end
-
-
1
def can_reassign_leader?(user)
-
# 活动创建者可以重新分配
-
return true if event.leader == user
-
-
# 或者在权限窗口内的领读人
-
event.current_daily_leader?(user, schedule)
-
end
-
-
1
def schedule_needs_backup?(schedule)
-
# 检查是否缺少领读人
-
return true unless schedule.daily_leader.present?
-
-
# 检查是否缺少领读内容
-
if schedule.daily_leader.present? && !schedule.daily_leading.present?
-
return true
-
end
-
-
# 检查是否缺少小红花(如果有打卡的话)
-
if schedule.date <= Date.today && schedule.check_ins.any? && schedule.flowers.empty?
-
return true
-
end
-
-
false
-
end
-
-
1
def calculate_leader_workloads(participants)
-
workloads = participants.index_by(&:id).transform_values { 0 }
-
-
# 可以扩展为查询历史工作量
-
# 目前简化处理,所有参与者初始工作量为0
-
-
workloads
-
end
-
-
1
def calculate_leader_workload_statistics(schedules)
-
workload = {}
-
-
schedules.where.not(daily_leader: nil).each do |schedule|
-
leader_id = schedule.daily_leader_id
-
workload[leader_id] ||= {
-
nickname: schedule.daily_leader.nickname,
-
assigned_count: 0,
-
content_completed: 0,
-
flowers_given: 0
-
}
-
-
workload[leader_id][:assigned_count] += 1
-
workload[leader_id][:content_completed] += 1 if schedule.daily_leading.present?
-
workload[leader_id][:flowers_given] += schedule.flowers.count
-
end
-
-
workload.values
-
end
-
-
1
def calculate_content_completion_rate(schedules)
-
return 0 if schedules.empty?
-
-
completed_count = schedules.joins(:daily_leading).count
-
(completed_count.to_f / schedules.count * 100).round(2)
-
end
-
-
1
def backup_needed_schedules
-
event.schedules_need_backup || []
-
end
-
-
1
def user_leading_schedules
-
return [] unless user
-
-
event.reading_schedules
-
.where(daily_leader: user)
-
.includes(:daily_leading, :flowers, :check_ins)
-
.map do |schedule|
-
{
-
id: schedule.id,
-
day_number: schedule.day_number,
-
date: schedule.date,
-
has_content: schedule.daily_leading.present?,
-
flowers_count: schedule.flowers.count,
-
check_ins_count: schedule.check_ins.count
-
}
-
end
-
end
-
-
1
def get_permission_window_info
-
return {} unless user && schedule
-
-
{
-
can_publish_content: event.can_publish_leading_content?(user, schedule),
-
can_give_flowers: event.can_give_flowers?(user, schedule),
-
permission_deadline: schedule.date + 1.day
-
}
-
end
-
-
1
def schedule_info(schedule)
-
{
-
id: schedule.id,
-
day_number: schedule.day_number,
-
date: schedule.date,
-
reading_progress: schedule.reading_progress
-
}
-
end
-
-
1
def user_info(user)
-
{
-
id: user.id,
-
nickname: user.nickname,
-
avatar_url: user.avatar_url
-
}
-
end
-
-
1
def log_backup_assignment(schedule, backup_leader)
-
Rails.logger.info "补位分配: 活动 #{event.id}, 日程 #{schedule.id}, 补位人 #{backup_leader.nickname}"
-
end
-
-
1
def log_reassignment(schedule, old_leader, new_leader)
-
Rails.logger.info "重新分配: 活动 #{event.id}, 日程 #{schedule.id}, 原领读人 #{old_leader&.nickname}, 新领读人 #{new_leader.nickname}"
-
end
-
end
-
# frozen_string_literal: true
-
-
# ModerationNotificationService - 内容审核通知服务
-
# 专门负责内容审核相关的通知管理
-
class ModerationNotificationService < ApplicationService
-
include ServiceInterface
-
attr_reader :report, :notification_type, :options
-
-
def initialize(report:, notification_type:, options: {})
-
super()
-
@report = report
-
@notification_type = notification_type
-
@options = options.with_indifferent_access
-
end
-
-
# 发送通知
-
def call
-
handle_errors do
-
validate_notification_params
-
send_notifications
-
log_notification_activity
-
end
-
self
-
end
-
-
# 类方法:通知管理员有新举报
-
def self.notify_admins_of_new_report(report)
-
new(
-
report: report,
-
notification_type: :new_report
-
).call
-
end
-
-
# 类方法:通知举报人状态更新
-
def self.notify_reporter_of_status_change(report)
-
new(
-
report: report,
-
notification_type: :status_change
-
).call
-
end
-
-
# 类方法:通知内容作者
-
def self.notify_content_author(report, action_taken:)
-
new(
-
report: report,
-
notification_type: :content_action,
-
options: { action_taken: action_taken }
-
).call
-
end
-
-
# 类方法:发送每日审核摘要
-
def self.send_daily_summary(date = Date.current)
-
reports = ContentReport.where(created_at: date.all_day)
-
-
new(
-
report: nil,
-
notification_type: :daily_summary,
-
options: { date: date, reports: reports }
-
).call
-
end
-
-
private
-
-
# 验证通知参数
-
def validate_notification_params
-
case notification_type
-
when :new_report, :status_change, :content_action
-
return failure!("举报不能为空") unless report
-
return failure!("举报不存在") unless report.persisted?
-
when :daily_summary
-
# 这些通知类型不需要具体的report实例
-
else
-
return failure!("无效的通知类型")
-
end
-
-
true
-
end
-
-
# 发送通知
-
def send_notifications
-
case notification_type
-
when :new_report
-
notify_admins_new_report
-
when :status_change
-
notify_reporter_status_change
-
when :content_action
-
notify_content_author_action
-
when :daily_summary
-
send_daily_summary_notifications
-
end
-
-
true
-
end
-
-
# 通知管理员有新举报
-
def notify_admins_new_report
-
return unless Rails.env.production? || options[:force_notification]
-
-
admins = get_admin_users
-
return if admins.empty?
-
-
admins.each do |admin|
-
send_notification_to_admin(admin, :new_report, {
-
report_id: report.id,
-
reason: report.reason,
-
reporter: report.user&.nickname,
-
content_preview: get_content_preview
-
})
-
end
-
end
-
-
# 通知举报人状态更新
-
def notify_reporter_status_change
-
return unless Rails.env.production? || options[:force_notification]
-
return unless report.user
-
-
send_notification_to_user(report.user, :report_status_change, {
-
report_id: report.id,
-
status: report.status,
-
admin_notes: report.notes,
-
processed_at: report.updated_at
-
})
-
end
-
-
# 通知内容作者
-
def notify_content_author_action
-
return unless Rails.env.production? || options[:force_notification]
-
return unless report&.target_content&.user
-
-
content_author = report.target_content.user
-
return if content_author == report.user # 不通知自己举报自己的情况
-
-
send_notification_to_user(content_author, :content_moderation_action, {
-
content_id: report.target_content.id,
-
content_type: report.target_content.class.name,
-
action_taken: options[:action_taken],
-
reason: report.reason,
-
moderator: report.admin&.nickname
-
})
-
end
-
-
# 发送每日审核摘要
-
def send_daily_summary_notifications
-
return unless Rails.env.production? || options[:force_notification]
-
-
date = options[:date]
-
reports = options[:reports]
-
-
return if reports.empty?
-
-
admins = get_admin_users
-
return if admins.empty?
-
-
summary_data = generate_daily_summary(date, reports)
-
-
admins.each do |admin|
-
send_notification_to_admin(admin, :daily_summary, summary_data)
-
end
-
end
-
-
# 发送通知给管理员
-
def send_notification_to_admin(admin, type, data)
-
# 这里可以集成多种通知方式
-
send_in_app_notification(admin, type, data)
-
send_email_notification(admin, type, data) if should_send_email?(admin, type)
-
send_push_notification(admin, type, data) if should_send_push?(admin, type)
-
end
-
-
# 发送通知给用户
-
def send_notification_to_user(user, type, data)
-
# 用户通知主要使用应用内通知
-
send_in_app_notification(user, type, data)
-
send_email_notification(user, type, data) if should_send_email_to_user?(user, type)
-
end
-
-
# 发送应用内通知
-
def send_in_app_notification(user, type, data)
-
notification_data = build_notification_data(type, data)
-
-
# 这里应该调用通知服务创建应用内通知
-
# NotificationService.create_notification(user, notification_data)
-
-
Rails.logger.info "In-app notification created for #{user.nickname}: #{type} - #{notification_data[:title]}"
-
end
-
-
# 发送邮件通知
-
def send_email_notification(user, type, data)
-
return unless user.respond_to?(:email) && user.email.present?
-
-
# 这里应该调用邮件服务发送邮件
-
# EmailService.send_moderation_notification(user, type, data)
-
-
Rails.logger.info "Email notification sent to #{user.email}: #{type}"
-
end
-
-
# 发送推送通知
-
def send_push_notification(user, type, data)
-
# 这里应该调用推送服务发送推送
-
# PushService.send_notification(user, build_push_data(type, data))
-
-
Rails.logger.info "Push notification sent to #{user.nickname}: #{type}"
-
end
-
-
# 构建通知数据
-
def build_notification_data(type, data)
-
case type
-
when :new_report
-
{
-
title: '新内容举报',
-
message: "#{data[:reporter]} 举报了内容:#{data[:reason]}",
-
url: "/admin/content_reports/#{data[:report_id]}",
-
priority: 'high'
-
}
-
when :report_status_change
-
{
-
title: '举报状态更新',
-
message: "您的举报已#{data[:status]},管理员备注:#{data[:admin_notes]}",
-
url: "/user/reports/#{data[:report_id]}"
-
}
-
when :content_moderation_action
-
action_text = data[:action_taken] == 'hidden' ? '已被隐藏' : '已被处理'
-
{
-
title: '内容审核通知',
-
message: "您的内容#{action_text},原因:#{data[:reason]}",
-
url: "/user/content/#{data[:content_id]}"
-
}
-
when :daily_summary
-
{
-
title: '每日审核摘要',
-
message: "昨日共收到#{data[:total_reports]}个举报,已处理#{data[:processed_reports]}个",
-
url: "/admin/analytics/content_moderation"
-
}
-
else
-
{
-
title: '内容审核通知',
-
message: '您有新的内容审核相关信息'
-
}
-
end
-
end
-
-
# 生成每日摘要数据
-
def generate_daily_summary(date, reports)
-
{
-
date: date,
-
total_reports: reports.count,
-
pending_reports: reports.pending.count,
-
processed_reports: reports.where.not(status: :pending).count,
-
by_reason: reports.group(:reason).count,
-
high_priority_reports: reports.where(reason: %w[sensitive_words harassment]).count
-
}
-
end
-
-
# 获取管理员用户
-
def get_admin_users
-
User.where(role: 1).or(User.where(role: 'admin'))
-
end
-
-
# 获取内容预览
-
def get_content_preview
-
return nil unless report&.target_content&.respond_to?(:content)
-
-
content = report.target_content.content
-
content ? content.truncate(100) : ''
-
end
-
-
# 判断是否应该发送邮件
-
def should_send_email?(user, type)
-
return false unless user.respond_to?(:email_notifications)
-
return false unless user.email_notifications?
-
return false unless user.respond_to?(:moderation_email_notifications)
-
-
case type
-
when :new_report
-
user.moderation_email_notifications?
-
when :daily_summary
-
user.daily_summary_emails?
-
else
-
true
-
end
-
end
-
-
# 判断是否应该向用户发送邮件
-
def should_send_email_to_user?(user, type)
-
return false unless user.respond_to?(:email_notifications)
-
return false unless user.email_notifications?
-
-
case type
-
when :report_status_change
-
user.report_status_email_notifications?
-
else
-
true
-
end
-
end
-
-
# 判断是否应该发送推送通知
-
def should_send_push?(user, type)
-
return false unless user.respond_to?(:push_notifications)
-
return false unless user.push_notifications?
-
-
case type
-
when :new_report
-
true # 高优先级通知
-
when :daily_summary
-
false # 摘要通知不需要推送
-
else
-
user.moderation_push_notifications?
-
end
-
end
-
-
# 记录通知活动日志
-
def log_notification_activity
-
case notification_type
-
when :new_report
-
Rails.logger.info "Admins notified of new content report: Report##{report.id}"
-
when :status_change
-
Rails.logger.info "Reporter notified of status change: Report##{report.id} -> #{report.status}"
-
when :content_action
-
Rails.logger.info "Content author notified of moderation action: Report##{report.id}"
-
when :daily_summary
-
Rails.logger.info "Daily moderation summary sent for #{options[:date]}"
-
end
-
end
-
end
-
# 通知服务
-
# 负责管理系统中各种用户通知的创建、发送和管理
-
class NotificationService < ApplicationService
-
include ServiceInterface
-
attr_reader :recipient, :actor, :notifiable, :notification_type, :title, :content
-
-
def initialize(recipient:, actor:, notifiable:, notification_type:, title:, content:)
-
super()
-
@recipient = recipient
-
@actor = actor
-
@notifiable = notifiable
-
@notification_type = notification_type
-
@title = title
-
@content = content
-
end
-
-
def call
-
handle_errors do
-
validate_parameters
-
create_notification
-
log_notification_created
-
self
-
end
-
end
-
-
# 类方法:小红花相关通知
-
class << self
-
# 发送小红花通知
-
def send_flower_notification(recipient, actor, flower)
-
return false if should_skip_notification?(recipient, actor, Notification::NOTIFICATION_TYPES[:flower_received])
-
-
notification = Notification.create_flower_notification(recipient, actor, flower)
-
log_notification("小红花通知", notification)
-
notification
-
end
-
-
# 发送评论通知
-
def send_comment_notification(recipient, actor, comment)
-
return false if should_skip_notification?(recipient, actor, Notification::NOTIFICATION_TYPES[:flower_comment])
-
-
notification = Notification.create_comment_notification(recipient, actor, comment)
-
log_notification("评论通知", notification)
-
notification
-
end
-
-
# 发送活动更新通知
-
def send_activity_update_notification(recipient, actor, event, update_type, message)
-
return false if should_skip_notification?(recipient, actor, Notification::NOTIFICATION_TYPES[:activity_update])
-
-
notification = Notification.create_activity_notification(recipient, actor, event, update_type, message)
-
log_notification("活动更新通知", notification)
-
notification
-
end
-
-
# 发送活动审批通知
-
def send_event_approval_notification(recipient, actor, event, approved)
-
notification_type = approved ? Notification::NOTIFICATION_TYPES[:event_approved] : Notification::NOTIFICATION_TYPES[:event_rejected]
-
return false if should_skip_notification?(recipient, actor, notification_type)
-
-
notification = Notification.create_event_approval_notification(recipient, actor, event, approved)
-
log_notification("活动审批通知", notification)
-
notification
-
end
-
-
# 批量发送通知
-
def send_bulk_notifications(recipients, actor, notifiable, notification_type, title, content)
-
return [] if recipients.blank?
-
-
notifications = []
-
recipients.each do |recipient|
-
next if should_skip_notification?(recipient, actor, notification_type)
-
-
notification = Notification.create!(
-
recipient: recipient,
-
actor: actor,
-
notifiable: notifiable,
-
notification_type: notification_type,
-
title: title,
-
content: content
-
)
-
notifications << notification
-
end
-
-
log_bulk_notification(notification_type, notifications.count)
-
notifications
-
end
-
-
# 发送系统通知
-
def send_system_notification(recipients, title, content, options = {})
-
actor = options[:actor] || User.find_by(role: 2) # root admin or default actor
-
notifiable = options[:notifiable]
-
-
notifications = []
-
Array(recipients).each do |recipient|
-
notification = Notification.create!(
-
recipient: recipient,
-
actor: actor,
-
notifiable: notifiable || recipient, # 如果没有指定notifiable,使用recipient作为默认值
-
notification_type: 'activity_update',
-
title: title,
-
content: content
-
)
-
notifications << notification
-
end
-
-
log_notification("系统通知", notifications)
-
notifications
-
end
-
-
# 获取用户未读通知数量
-
def unread_count_for(user)
-
Notification.unread_count_for(user)
-
end
-
-
# 获取用户最近的通知
-
def recent_notifications_for(user, limit = 10, include_read: false)
-
scope = Notification.for_recipient(user).recent.limit(limit)
-
scope = scope.unread unless include_read
-
scope
-
end
-
-
# 标记通知为已读
-
def mark_as_read(notification_id, user)
-
notification = Notification.find_by(id: notification_id, recipient: user)
-
return false unless notification
-
-
notification.mark_as_read!
-
true
-
end
-
-
# 批量标记为已读
-
def mark_all_as_read_for(user)
-
Notification.mark_all_as_read_for(user)
-
log_notification_action("批量标记已读", user: user.id)
-
end
-
-
# 删除通知
-
def delete_notification(notification_id, user)
-
notification = Notification.find_by(id: notification_id, recipient: user)
-
return false unless notification
-
-
notification.destroy
-
log_notification_action("删除通知", user: user.id, notification: notification_id)
-
true
-
end
-
-
# 批量删除通知
-
def delete_notifications(notification_ids, user)
-
notifications = Notification.where(id: notification_ids, recipient: user)
-
deleted_count = notifications.count
-
notifications.destroy_all
-
-
log_notification_action("批量删除通知", user: user.id, count: deleted_count)
-
deleted_count
-
end
-
-
# 清理过期通知
-
def cleanup_old_notifications(days = 30)
-
deleted_count = Notification.cleanup_old_notifications(days)
-
log_notification_action("清理过期通知", days: days, count: deleted_count)
-
deleted_count
-
end
-
-
# 获取通知统计
-
def notification_stats_for(user, days = 7)
-
notifications = Notification.for_recipient(user)
-
.where('created_at >= ?', days.days.ago)
-
-
{
-
total_count: notifications.count,
-
unread_count: notifications.unread.count,
-
by_type: notifications.group(:notification_type).count,
-
recent_count: notifications.where('created_at >= ?', 1.day.ago).count
-
}
-
end
-
-
# 检查用户是否有新通知
-
def has_new_notifications?(user, since: nil)
-
scope = Notification.for_recipient(user).unread
-
scope = scope.where('created_at > ?', since) if since
-
scope.exists?
-
end
-
-
# 获取用户的通知偏好设置(预留接口)
-
def notification_preferences(user)
-
# TODO: 实现用户通知偏好设置
-
{
-
flower_received: true,
-
flower_comment: true,
-
activity_update: true,
-
event_approved: true,
-
event_rejected: true
-
}
-
end
-
-
# 检查用户是否接收某种类型的通知
-
def should_receive_notification?(user, notification_type)
-
preferences = notification_preferences(user)
-
preferences[notification_type.to_sym]
-
end
-
-
private
-
-
# 判断是否应该跳过通知发送
-
def should_skip_notification?(recipient, actor, notification_type = nil)
-
return true if recipient.nil? || actor.nil?
-
return true if recipient.id == actor.id # 不给自己发通知
-
return true unless notification_type && should_receive_notification?(recipient, notification_type)
-
false
-
end
-
-
# 记录通知日志
-
def log_notification(type, notification)
-
if notification.is_a?(Array)
-
Rails.logger.info "批量通知创建成功: #{type} - 数量: #{notification.count}"
-
else
-
Rails.logger.info "通知创建成功: #{type} - 接收者: #{notification.recipient&.id}, 类型: #{notification.notification_type}"
-
end
-
end
-
-
# 记录批量通知日志
-
def log_bulk_notification(type, count)
-
Rails.logger.info "批量通知创建成功: #{type} - 数量: #{count}"
-
end
-
-
# 记录通知操作日志
-
def log_notification_action(action, **params)
-
Rails.logger.info "通知操作: #{action} - #{params}"
-
end
-
end
-
-
private
-
-
# 验证参数
-
def validate_parameters
-
errors.add(:recipient, "接收者不能为空") if recipient.blank?
-
errors.add(:actor, "发送者不能为空") if actor.blank?
-
errors.add(:notification_type, "通知类型不能为空") if notification_type.blank?
-
errors.add(:title, "标题不能为空") if title.blank?
-
errors.add(:content, "内容不能为空") if content.blank?
-
-
# 验证通知类型
-
unless Notification::NOTIFICATION_TYPES.values.include?(notification_type)
-
errors.add(:notification_type, "无效的通知类型")
-
end
-
-
# 验证接收者和发送者不是同一人
-
if recipient.present? && actor.present? && recipient.id == actor.id
-
errors.add(:base, "不能给自己发送通知")
-
end
-
end
-
-
# 创建通知
-
def create_notification
-
@notification = Notification.create!(
-
recipient: recipient,
-
actor: actor,
-
notifiable: notifiable,
-
notification_type: notification_type,
-
title: title,
-
content: content
-
)
-
end
-
-
# 记录通知创建日志
-
def log_notification_created
-
Rails.logger.info "通知创建成功: 类型: #{notification_type}, 接收者: #{recipient.id}, 发送者: #{actor.id}"
-
end
-
end
-
# frozen_string_literal: true
-
-
# OptimizedPaginationService - 高性能分页服务
-
# 使用cursor-based pagination避免OFFSET性能问题
-
class OptimizedPaginationService < ApplicationService
-
include ServiceInterface
-
-
attr_reader :relation, :page, :per_page, :cursor, :order_field, :order_direction
-
-
def initialize(relation:, page: nil, per_page: 20, cursor: nil, order_field: :created_at, order_direction: :desc)
-
super()
-
@relation = relation
-
@page = page
-
@per_page = per_page
-
@cursor = cursor
-
@order_field = order_field
-
@order_direction = order_direction
-
end
-
-
def call
-
handle_errors do
-
validate_parameters
-
paginate
-
end
-
self
-
end
-
-
# 类方法:快速分页
-
def self.paginate(relation, page: 1, per_page: 20, cursor: nil, order_field: :created_at, order_direction: :desc)
-
new(
-
relation: relation,
-
page: page,
-
per_page: per_page,
-
cursor: cursor,
-
order_field: order_field,
-
order_direction: order_direction
-
).call
-
end
-
-
# 类方法:cursor-based分页(适用于无限滚动)
-
def self.cursor_paginate(relation, cursor: nil, per_page: 20, order_field: :created_at, order_direction: :desc)
-
new(
-
relation: relation,
-
cursor: cursor,
-
per_page: per_page,
-
order_field: order_field,
-
order_direction: order_direction
-
).call
-
end
-
-
def data
-
@data ||= {}
-
end
-
-
def has_next_page?
-
data[:has_next_page]
-
end
-
-
def has_prev_page?
-
data[:has_prev_page]
-
end
-
-
def next_cursor
-
data[:next_cursor]
-
end
-
-
def prev_cursor
-
data[:prev_cursor]
-
end
-
-
def total_count
-
data[:total_count]
-
end
-
-
private
-
-
def validate_parameters
-
errors.add(:relation, "查询对象不能为空") if relation.blank?
-
errors.add(:per_page, "每页数量必须大于0") if per_page.to_i <= 0
-
errors.add(:per_page, "每页数量不能超过100") if per_page.to_i > 100
-
-
if cursor && page
-
errors.add(:base, "不能同时使用cursor和page分页")
-
end
-
-
# 验证排序字段是否存在
-
if order_field.present? && !relation.column_names.include?(order_field.to_s)
-
errors.add(:order_field, "无效的排序字段")
-
end
-
end
-
-
def paginate
-
if cursor.present?
-
cursor_based_pagination
-
else
-
offset_based_pagination
-
end
-
end
-
-
# 基于OFFSET的传统分页
-
def offset_based_pagination
-
page_num = [page.to_i, 1].max
-
offset_value = (page_num - 1) * per_page
-
-
# 获取总记录数(可选,用于显示分页信息)
-
if should_count_total?
-
total_records = relation.count
-
else
-
total_records = nil
-
end
-
-
# 执行分页查询
-
paginated_relation = relation
-
.limit(per_page + 1) # 多查询一条用于判断是否有下一页
-
.offset(offset_value)
-
.order(order_direction_sql)
-
-
records = paginated_relation.to_a
-
has_next = records.length > per_page
-
records.pop if has_next # 移除多查询的记录
-
-
data.merge!({
-
records: records,
-
current_page: page_num,
-
per_page: per_page,
-
has_next_page: has_next,
-
has_prev_page: page_num > 1,
-
total_count: total_records,
-
total_pages: total_records ? (total_records.to_f / per_page).ceil : nil
-
})
-
-
self
-
end
-
-
# 基于cursor的高性能分页
-
def cursor_based_pagination
-
# 解析cursor
-
cursor_value = decode_cursor(cursor) if cursor
-
-
# 构建查询条件
-
query_relation = relation
-
if cursor_value
-
query_relation = query_relation.where(cursor_condition(cursor_value))
-
end
-
-
# 执行查询,多查询一条用于判断是否有下一页
-
paginated_relation = query_relation
-
.limit(per_page + 1)
-
.order(order_direction_sql)
-
-
records = paginated_relation.to_a
-
has_next = records.length > per_page
-
records.pop if has_next
-
-
# 生成cursor信息
-
next_cursor_value = records.last ? records.last[order_field] : nil
-
prev_cursor_value = records.first ? records.first[order_field] : nil
-
-
data.merge!({
-
records: records,
-
per_page: per_page,
-
has_next_page: has_next,
-
has_prev_page: cursor.present?,
-
next_cursor: next_cursor_value ? encode_cursor(next_cursor_value) : nil,
-
prev_cursor: prev_cursor_value ? encode_cursor(prev_cursor_value) : nil
-
})
-
-
self
-
end
-
-
def order_direction_sql
-
case order_direction.to_sym
-
when :asc
-
"#{order_field} ASC"
-
when :desc
-
"#{order_field} DESC"
-
else
-
"#{order_field} DESC" # 默认降序
-
end
-
end
-
-
def cursor_condition(cursor_value)
-
case order_direction.to_sym
-
when :asc
-
"#{order_field} > ?"
-
when :desc
-
"#{order_field} < ?"
-
else
-
"#{order_field} < ?" # 默认降序
-
end
-
end
-
-
def encode_cursor(value)
-
# Base64编码cursor值
-
Base64.urlsafe_encode64("#{value}:#{Time.current.to_i}")
-
end
-
-
def decode_cursor(encoded_cursor)
-
return nil unless encoded_cursor
-
-
begin
-
decoded = Base64.urlsafe_decode64(encoded_cursor)
-
decoded.split(':').first
-
rescue
-
nil
-
end
-
end
-
-
def should_count_total?
-
# 只有在第一页时才计算总数,避免性能问题
-
page.to_i <= 1 && !cursor
-
end
-
end
-
# frozen_string_literal: true
-
-
# 分页服务
-
# 提供多种分页策略,优化大数据集的查询性能
-
class PaginationService
-
class << self
-
# 基于偏移量的传统分页
-
# @param scope [ActiveRecord::Relation] 查询范围
-
# @param page [Integer] 页码(从1开始)
-
# @param per_page [Integer] 每页记录数
-
# @param options [Hash] 额外选项
-
# @return [Hash] 分页结果
-
def paginate_by_offset(scope, page: 1, per_page: 20, options = {})
-
page = [page.to_i, 1].max
-
per_page = [[per_page.to_i, 1].max, 100].min # 限制最大每页100条
-
-
total_count = QueryOptimizationService.optimized_count_query(
-
scope,
-
options[:cache_key] ? "count_#{options[:cache_key]}" : nil,
-
options[:cache_ttl] || 5.minutes
-
)
-
-
total_pages = (total_count.to_f / per_page).ceil
-
offset = (page - 1) * per_page
-
-
records = scope.limit(per_page).offset(offset)
-
records = QueryOptimizationService.preload_associations(records, options[:includes]) if options[:includes]
-
-
{
-
records: records,
-
pagination: {
-
current_page: page,
-
per_page: per_page,
-
total_count: total_count,
-
total_pages: total_pages,
-
has_next_page: page < total_pages,
-
has_prev_page: page > 1,
-
next_page: page < total_pages ? page + 1 : nil,
-
prev_page: page > 1 ? page - 1 : nil
-
}
-
}
-
end
-
-
# 基于游标的分页(性能更好,适合大数据集)
-
# @param scope [ActiveRecord::Relation] 查询范围
-
# @param cursor [String] 游标位置
-
# @param limit [Integer] 每页记录数
-
# @param options [Hash] 额外选项
-
# @return [Hash] 分页结果
-
def paginate_by_cursor(scope, cursor: nil, limit: 20, options = {})
-
limit = [[limit.to_i, 1].max, 100].min
-
order_column = options[:order_column] || 'created_at'
-
order_direction = options[:order_direction] || 'desc'
-
-
# 构建查询
-
query = scope.limit(limit + 1) # 多查询一条来判断是否还有下一页
-
-
# 添加游标条件
-
if cursor
-
operator = order_direction == 'desc' ? '<' : '>'
-
query = query.where("#{order_column} #{operator} ?", decode_cursor(cursor))
-
end
-
-
# 排序
-
query = query.order("#{order_column} #{order_direction}")
-
-
# 预加载关联
-
if options[:includes]
-
records = QueryOptimizationService.preload_associations(query, options[:includes])
-
else
-
records = query.to_a
-
end
-
-
# 判断是否还有下一页
-
has_next = records.length > limit
-
records = records.first(limit) if has_next
-
-
# 生成下一页游标
-
next_cursor = nil
-
if has_next && records.any?
-
last_record = records.last
-
next_cursor = encode_cursor(last_record.send(order_column))
-
end
-
-
{
-
records: records,
-
pagination: {
-
next_cursor: next_cursor,
-
has_next_page: has_next,
-
limit: limit,
-
order_column: order_column,
-
order_direction: order_direction
-
}
-
}
-
end
-
-
# 搜索分页(结合搜索和分页)
-
# @param scope [ActiveRecord::Relation] 查询范围
-
# @param search_term [String] 搜索关键词
-
# @param search_fields [Array] 搜索字段
-
# @param pagination_options [Hash] 分页选项
-
# @return [Hash] 搜索分页结果
-
def search_and_paginate(scope, search_term: nil, search_fields: [], pagination_options: {})
-
# 应用搜索条件
-
if search_term.present? && search_fields.any?
-
search_conditions = search_fields.map do |field|
-
"#{field} ILIKE ?"
-
end.join(' OR ')
-
-
search_values = search_fields.map { search_term }
-
scope = scope.where(search_conditions, *search_values)
-
end
-
-
# 执行分页
-
if pagination_options[:cursor]
-
paginate_by_cursor(scope, pagination_options)
-
else
-
paginate_by_offset(scope, pagination_options)
-
end
-
end
-
-
# 无限滚动分页(适合移动端)
-
# @param scope [ActiveRecord::Relation] 查询范围
-
# @param last_id [Integer] 上一页最后一条记录的ID
-
# @param limit [Integer] 加载记录数
-
# @param options [Hash] 额外选项
-
# @return [Hash] 分页结果
-
def infinite_scroll(scope, last_id: nil, limit: 20, options = {})
-
limit = [[limit.to_i, 1].max, 50].min # 无限滚动通常限制更多
-
-
query = scope.limit(limit + 1)
-
-
# 添加ID条件
-
if last_id
-
if options[:order_direction] == 'asc'
-
query = query.where('id > ?', last_id)
-
else
-
query = query.where('id < ?', last_id)
-
end
-
end
-
-
# 排序
-
order_direction = options[:order_direction] || 'desc'
-
query = query.order("id #{order_direction}")
-
-
# 预加载关联
-
if options[:includes]
-
records = QueryOptimizationService.preload_associations(query, options[:includes])
-
else
-
records = query.to_a
-
end
-
-
# 判断是否还有更多数据
-
has_more = records.length > limit
-
records = records.first(limit) if has_more
-
-
# 下一页的最后ID
-
next_last_id = nil
-
if has_more && records.any?
-
next_last_id = records.last.id
-
end
-
-
{
-
records: records,
-
pagination: {
-
next_last_id: next_last_id,
-
has_more: has_more,
-
limit: limit
-
}
-
}
-
end
-
-
# 时间范围分页
-
# @param scope [ActiveRecord::Relation] 查询范围
-
# @param time_field [String] 时间字段名
-
# @param start_time [DateTime] 开始时间
-
# @param end_time [DateTime] 结束时间
-
# @param pagination_options [Hash] 分页选项
-
# @return [Hash] 时间范围分页结果
-
def paginate_by_time_range(scope, time_field: 'created_at', start_time: nil, end_time: nil, pagination_options: {})
-
query = scope
-
-
# 应用时间范围过滤
-
if start_time
-
query = query.where("#{time_field} >= ?", start_time)
-
end
-
-
if end_time
-
query = query.where("#{time_field} <= ?", end_time)
-
end
-
-
# 按时间字段排序
-
query = query.order("#{time_field} DESC")
-
-
# 执行分页
-
if pagination_options[:cursor]
-
# 基于时间的游标分页
-
cursor_based_time_pagination(query, time_field, pagination_options)
-
else
-
# 传统分页
-
paginate_by_offset(query, pagination_options)
-
end
-
end
-
-
# 分组分页(按某个字段分组后分页)
-
# @param scope [ActiveRecord::Relation] 查询范围
-
# @param group_field [String] 分组字段
-
# @param pagination_options [Hash] 分页选项
-
# @return [Hash] 分组分页结果
-
def paginate_by_group(scope, group_field, pagination_options = {})
-
per_page = pagination_options[:per_page] || 10
-
page = pagination_options[:page] || 1
-
-
# 获取分组数据
-
grouped_data = scope.group(group_field)
-
.select("#{group_field}, COUNT(*) as count")
-
.order("COUNT(*) DESC")
-
.to_a
-
-
# 分页处理分组
-
total_groups = grouped_data.length
-
total_pages = (total_groups.to_f / per_page).ceil
-
offset = (page - 1) * per_page
-
-
paginated_groups = grouped_data[offset, per_page] || []
-
-
# 获取每个分组的详细记录
-
records = []
-
paginated_groups.each do |group|
-
group_records = scope.where(group_field => group.send(group_field))
-
.limit(pagination_options[:per_group_limit] || 5)
-
records.concat(group_records)
-
end
-
-
{
-
records: records,
-
groups: paginated_groups,
-
pagination: {
-
current_page: page,
-
per_page: per_page,
-
total_groups: total_groups,
-
total_pages: total_pages,
-
has_next_page: page < total_pages,
-
has_prev_page: page > 1
-
}
-
}
-
end
-
-
# 元数据分页(只返回分页信息,不返回具体记录)
-
# @param scope [ActiveRecord::Relation] 查询范围
-
# @param per_page [Integer] 每页记录数
-
# @param options [Hash] 额外选项
-
# @return [Hash] 分页元数据
-
def pagination_metadata(scope, per_page: 20, options = {})
-
total_count = QueryOptimizationService.optimized_count_query(
-
scope,
-
options[:cache_key] ? "metadata_#{options[:cache_key]}" : nil,
-
options[:cache_ttl] || 10.minutes
-
)
-
-
total_pages = (total_count.to_f / per_page).ceil
-
-
{
-
total_count: total_count,
-
total_pages: total_pages,
-
per_page: per_page,
-
first_page: 1,
-
last_page: total_pages,
-
page_range: calculate_page_range(total_pages, options[:current_page] || 1)
-
}
-
end
-
-
private
-
-
# 编码游标
-
def encode_cursor(value)
-
Base64.urlsafe_encode64(value.to_s)
-
end
-
-
# 解码游标
-
def decode_cursor(cursor)
-
Base64.urlsafe_decode64(cursor)
-
rescue
-
nil
-
end
-
-
# 基于时间的游标分页
-
def cursor_based_time_pagination(scope, time_field, options)
-
cursor = options[:cursor]
-
limit = options[:limit] || 20
-
-
query = scope.limit(limit + 1)
-
-
if cursor
-
decoded_time = decode_cursor(cursor)
-
query = query.where("#{time_field} < ?", decoded_time) if decoded_time
-
end
-
-
query = query.order("#{time_field} DESC")
-
records = query.to_a
-
-
has_next = records.length > limit
-
records = records.first(limit) if has_next
-
-
next_cursor = nil
-
if has_next && records.any?
-
next_cursor = encode_cursor(records.last.send(time_field).iso8601)
-
end
-
-
{
-
records: records,
-
pagination: {
-
next_cursor: next_cursor,
-
has_next_page: has_next,
-
limit: limit
-
}
-
}
-
end
-
-
# 计算页码范围(用于显示页码导航)
-
def calculate_page_range(total_pages, current_page, window_size: 5)
-
return [] if total_pages == 0
-
-
start_page = [current_page - window_size / 2, 1].max
-
end_page = [start_page + window_size - 1, total_pages].min
-
start_page = [end_page - window_size + 1, 1].max if end_page - start_page + 1 < window_size
-
-
(start_page..end_page).to_a
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# PermissionCheckService - 统一权限验证服务
-
# 整合各种权限检查逻辑,提供统一的权限验证接口
-
class PermissionCheckService < ApplicationService
-
attr_reader :user, :resource, :action, :context
-
-
def initialize(user:, resource:, action:, context: {})
-
super()
-
@user = user
-
@resource = resource
-
@action = action
-
@context = context
-
end
-
-
# 主要调用方法
-
def call
-
handle_errors do
-
return failure!("用户不能为空") unless user
-
return failure!("资源不能为空") unless resource
-
-
case action
-
when :approve_events
-
check_approve_events_permission
-
when :manage_users
-
check_manage_users_permission
-
when :view_admin_panel
-
check_view_admin_panel_permission
-
when :manage_system
-
check_manage_system_permission
-
when :edit_post
-
check_edit_post_permission
-
when :hide_post
-
check_hide_post_permission
-
when :pin_post
-
check_pin_post_permission
-
when :manage_event
-
check_manage_event_permission
-
when :claim_leadership
-
check_claim_leadership_permission
-
when :complete_event
-
check_complete_event_permission
-
else
-
failure!("不支持的权限检查: #{action}")
-
end
-
end
-
end
-
-
# 类方法:快速权限检查
-
def self.can?(user, resource, action, context = {})
-
new(user: user, resource: resource, action: action, context: context).call.success?
-
end
-
-
private
-
-
# 检查活动审批权限
-
def check_approve_events_permission
-
if user.can_approve_events?
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有审批活动的权限")
-
end
-
end
-
-
# 检查用户管理权限
-
def check_manage_users_permission
-
if user.can_manage_users?
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有管理用户的权限")
-
end
-
end
-
-
# 检查管理面板查看权限
-
def check_view_admin_panel_permission
-
if user.can_view_admin_panel?
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有查看管理面板的权限")
-
end
-
end
-
-
# 检查系统管理权限
-
def check_manage_system_permission
-
if user.can_manage_system?
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有系统管理权限")
-
end
-
end
-
-
# 检查帖子编辑权限
-
def check_edit_post_permission
-
if resource.is_a?(Post)
-
if resource.can_edit?(user)
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有编辑此帖子的权限")
-
end
-
else
-
failure!("资源类型不正确,期望Post")
-
end
-
end
-
-
# 检查帖子隐藏权限
-
def check_hide_post_permission
-
if resource.is_a?(Post)
-
if resource.can_hide?(user)
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有隐藏此帖子的权限")
-
end
-
else
-
failure!("资源类型不正确,期望Post")
-
end
-
end
-
-
# 检查帖子置顶权限
-
def check_pin_post_permission
-
if resource.is_a?(Post)
-
if resource.can_pin?(user)
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有置顶此帖子的权限")
-
end
-
else
-
failure!("资源类型不正确,期望Post")
-
end
-
end
-
-
# 检查活动管理权限
-
def check_manage_event_permission
-
if resource.is_a?(ReadingEvent)
-
# 活动创建者或管理员可以管理活动
-
if resource.leader_id == user.id || user.any_admin?
-
success!
-
else
-
failure!("用户 #{user.nickname} 没有管理此活动的权限")
-
end
-
else
-
failure!("资源类型不正确,期望ReadingEvent")
-
end
-
end
-
-
# 检查领读报名权限
-
def check_claim_leadership_permission
-
if resource.is_a?(ReadingEvent) && context[:schedule]
-
schedule = context[:schedule]
-
-
# 检查是否是自由报名模式
-
unless resource.leader_assignment_type == 'voluntary'
-
return failure!("该活动不支持自由报名领读")
-
end
-
-
# 检查是否已报名该活动
-
unless user.enrollments.exists?(reading_event: resource)
-
return failure!("请先报名该活动")
-
end
-
-
# 检查是否已有领读人
-
if schedule.daily_leader.present?
-
return failure!("该日已有领读人")
-
end
-
-
# 检查领读次数限制
-
leadership_count = resource.reading_schedules.where(daily_leader: user).count
-
if leadership_count >= 3
-
return failure!("领读次数已达上限")
-
end
-
-
success!
-
else
-
failure!("资源类型或上下文不正确,期望ReadingEvent和schedule")
-
end
-
end
-
-
# 检查活动完成权限
-
def check_complete_event_permission
-
if resource.is_a?(ReadingEvent)
-
# 只有活动小组长可以结束活动
-
if resource.current_leader?(user)
-
success!
-
else
-
failure!("只有活动小组长可以结束活动")
-
end
-
else
-
failure!("资源类型不正确,期望ReadingEvent")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# PostCreationService - 帖子创建服务
-
# 专门负责帖子的创建逻辑,包括内容验证、分类处理等
-
class PostCreationService < ApplicationService
-
include ServiceInterface
-
attr_reader :user, :post_params, :post
-
-
def initialize(user:, post_params:)
-
super()
-
@user = user
-
@post_params = post_params
-
@post = nil
-
end
-
-
# 创建帖子
-
def call
-
handle_errors do
-
validate_creation_params
-
create_post
-
process_post_creation
-
format_success_response
-
end
-
self
-
end
-
-
private
-
-
# 验证创建参数
-
def validate_creation_params
-
return failure!("用户不能为空") unless user
-
return failure!("用户不存在") unless user.persisted?
-
return failure!("标题不能为空") if post_params[:title].blank?
-
return failure!("内容不能为空") if post_params[:content].blank?
-
-
# 验证内容长度
-
if post_params[:content].length < 10
-
return failure!("内容长度不能少于10个字符")
-
end
-
-
if post_params[:content].length > 10000
-
return failure!("内容长度不能超过10000个字符")
-
end
-
-
# 验证标题长度
-
if post_params[:title].length > 100
-
return failure!("标题长度不能超过100个字符")
-
end
-
end
-
-
# 创建帖子记录
-
def create_post
-
@post = user.posts.new(post_params)
-
-
unless @post.save
-
failure!(@post.errors.full_messages)
-
return false
-
end
-
-
true
-
end
-
-
# 处理帖子创建后的逻辑
-
def process_post_creation
-
# 处理标签
-
process_tags if @post.tags.present?
-
-
# 处理图片
-
process_images if @post.images.present?
-
-
# 更新用户统计
-
update_user_stats
-
-
# 记录创建日志
-
log_creation_event
-
-
# 发送通知(如果需要)
-
send_creation_notifications
-
end
-
-
# 处理标签
-
def process_tags
-
# 标签规范化处理
-
tags = @post.tags.map(&:strip).reject(&:blank?).uniq
-
@post.update!(tags: tags)
-
end
-
-
# 处理图片
-
def process_images
-
# 图片URL验证和处理
-
valid_images = @post.images.select { |url| valid_image_url?(url) }
-
@post.update!(images: valid_images) if valid_images.size != @post.images.size
-
end
-
-
# 验证图片URL
-
def valid_image_url?(url)
-
uri = URI.parse(url)
-
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
-
rescue URI::InvalidURIError
-
false
-
end
-
-
# 更新用户统计
-
def update_user_stats
-
# 检查用户模型是否有posts_count字段
-
user.increment!(:posts_count) if user.respond_to?(:posts_count)
-
end
-
-
# 记录创建事件
-
def log_creation_event
-
Rails.logger.info "Post created: ID #{@post.id} by User #{user.id}"
-
end
-
-
# 发送创建通知
-
def send_creation_notifications
-
# 这里可以添加通知逻辑,比如通知关注者
-
# NotificationService.post_created_notification(@post)
-
end
-
-
# 格式化成功响应
-
def format_success_response
-
success!({
-
message: "帖子创建成功",
-
post: post_data(@post)
-
})
-
end
-
-
# 格式化帖子数据
-
def post_data(post)
-
post.as_json_for_api(current_user: user, include_stats: true)
-
end
-
end
-
# frozen_string_literal: true
-
-
# PostDataService - 帖子数据服务
-
# 专门负责帖子数据的格式化、序列化和展示逻辑
-
class PostDataService < ApplicationService
-
include ServiceInterface
-
attr_reader :post, :current_user, :options
-
-
def initialize(post:, current_user: nil, options: {})
-
super()
-
@post = post
-
@current_user = current_user
-
@options = options.with_indifferent_access
-
end
-
-
# 格式化帖子数据
-
def call
-
handle_errors do
-
validate_data_params
-
format_post_data
-
end
-
self
-
end
-
-
# 生成帖子摘要
-
def generate_summary(length: 100)
-
return "" unless post&.content
-
-
# 移除HTML标签(如果有)
-
plain_content = post.content.gsub(/<[^>]+>/, '').strip
-
-
# 截取指定长度
-
if plain_content.length > length
-
plain_content[0...length] + "..."
-
else
-
plain_content
-
end
-
end
-
-
# 生成帖子统计信息
-
def generate_stats
-
return {} unless post
-
-
{
-
views_count: post.respond_to?(:views_count) ? post.views_count : 0,
-
likes_count: post.respond_to?(:likes_count) ? post.likes_count : 0,
-
comments_count: post.respond_to?(:comments_count) ? post.comments_count : 0,
-
shares_count: post.respond_to?(:shares_count) ? post.shares_count : 0,
-
bookmarks_count: post.respond_to?(:bookmarks_count) ? post.bookmarks_count : 0
-
}
-
end
-
-
# 生成时间相关数据
-
def generate_time_data
-
return {} unless post
-
-
{
-
created_at: post.created_at,
-
updated_at: post.updated_at,
-
time_ago: time_ago_in_words(post.created_at),
-
last_activity_ago: time_ago_in_words(post.updated_at)
-
}
-
end
-
-
# 生成作者信息
-
def generate_author_info
-
return {} unless post&.user
-
-
{
-
id: post.user.id,
-
nickname: post.user.nickname,
-
avatar_url: post.user.avatar_url,
-
role: post.user.role_display_name,
-
is_verified: post.user.respond_to?(:verified?) ? post.user.verified? : false,
-
followers_count: post.user.respond_to?(:followers_count) ? post.user.followers_count : 0,
-
posts_count: post.user.respond_to?(:posts_count) ? post.user.posts_count : 0
-
}
-
end
-
-
# 生成交互状态信息
-
def generate_interaction_states
-
return {} unless current_user && post
-
-
{
-
liked: post.liked_by?(current_user),
-
bookmarked: post.bookmarked_by?(current_user),
-
can_edit: PostPermissionService.can_edit?(post, current_user),
-
can_delete: PostPermissionService.can_delete?(post, current_user),
-
can_pin: PostPermissionService.can_pin?(post, current_user),
-
can_hide: PostPermissionService.can_hide?(post, current_user),
-
can_comment: PostPermissionService.can_comment?(post, current_user)
-
}
-
end
-
-
# 生成分类信息
-
def generate_category_info
-
return {} unless post
-
-
{
-
category: post.category,
-
category_name: post.category_name,
-
category_color: category_color(post.category)
-
}
-
end
-
-
# 生成标签信息
-
def generate_tags_info
-
return [] unless post&.tags
-
-
post.tags.map do |tag|
-
{
-
name: tag,
-
color: tag_color(tag),
-
count: tag_post_count(tag)
-
}
-
end
-
end
-
-
# 生成图片信息
-
def generate_images_info
-
return [] unless post&.images
-
-
post.images.map.with_index do |image_url, index|
-
{
-
url: image_url,
-
thumbnail: thumbnail_url(image_url),
-
alt: "#{post.title} - 图片#{index + 1}",
-
width: image_width(image_url),
-
height: image_height(image_url)
-
}
-
end
-
end
-
-
private
-
-
# 验证数据参数
-
def validate_data_params
-
return failure!("帖子不能为空") unless post
-
return failure!("帖子不存在") unless post.persisted?
-
true
-
end
-
-
# 格式化帖子数据
-
def format_post_data
-
data = {
-
id: post.id,
-
title: post.title,
-
content: formatted_content,
-
summary: generate_summary,
-
stats: generate_stats,
-
time_data: generate_time_data,
-
author: generate_author_info,
-
category: generate_category_info,
-
tags: generate_tags_info,
-
images: generate_images_info,
-
interactions: generate_interaction_states,
-
metadata: generate_metadata
-
}
-
-
# 根据选项添加额外字段
-
data.merge!(add_optional_fields)
-
-
success!(data)
-
end
-
-
# 格式化内容
-
def formatted_content
-
return post.content unless options[:format_content]
-
-
case options[:content_format]
-
when :html
-
format_content_as_html
-
when :markdown
-
format_content_as_markdown
-
when :plain
-
format_content_as_plain
-
else
-
post.content
-
end
-
end
-
-
# HTML格式化
-
def format_content_as_html
-
# 这里可以添加HTML格式化逻辑
-
post.content
-
end
-
-
# Markdown格式化
-
def format_content_as_markdown
-
# 这里可以添加Markdown格式化逻辑
-
post.content
-
end
-
-
# 纯文本格式化
-
def format_content_as_plain
-
post.content.gsub(/<[^>]+>/, '').strip
-
end
-
-
# 生成元数据
-
def generate_metadata
-
{
-
pinned: post.pinned?,
-
hidden: post.hidden?,
-
deleted: post.deleted?,
-
featured: post.featured?,
-
priority: post.priority || 0,
-
source: post.source || 'web',
-
device_type: post.device_type || 'unknown'
-
}
-
end
-
-
# 添加可选字段
-
def add_optional_fields
-
additional_fields = {}
-
-
# 包含完整内容
-
if options[:include_full_content]
-
additional_fields[:full_content] = post.content
-
end
-
-
# 包含SEO信息
-
if options[:include_seo]
-
additional_fields[:seo] = generate_seo_data
-
end
-
-
# 包含分享信息
-
if options[:include_share]
-
additional_fields[:share] = generate_share_data
-
end
-
-
# 包含相关帖子
-
if options[:include_related]
-
additional_fields[:related_posts] = generate_related_posts
-
end
-
-
additional_fields
-
end
-
-
# 生成SEO数据
-
def generate_seo_data
-
{
-
title: post.title,
-
description: generate_summary(length: 160),
-
keywords: post.tags&.join(', '),
-
url: post_url(post),
-
image_url: post.images&.first
-
}
-
end
-
-
# 生成分享数据
-
def generate_share_data
-
{
-
url: post_url(post),
-
title: post.title,
-
description: generate_summary,
-
image_url: post.images&.first
-
}
-
end
-
-
# 生成相关帖子
-
def generate_related_posts
-
# 这里可以添加相关帖子推荐逻辑
-
[]
-
end
-
-
# 辅助方法:时间格式化
-
def time_ago_in_words(time)
-
return "" unless time
-
-
seconds = Time.current - time
-
minutes = seconds / 60
-
hours = minutes / 60
-
days = hours / 24
-
-
if days > 0
-
"#{days.to_i}天前"
-
elsif hours > 0
-
"#{hours.to_i}小时前"
-
elsif minutes > 0
-
"#{minutes.to_i}分钟前"
-
else
-
"刚刚"
-
end
-
end
-
-
# 辅助方法:分类颜色
-
def category_color(category)
-
colors = {
-
'reading' => '#FF6B6B',
-
'discussion' => '#4ECDC4',
-
'share' => '#45B7D1',
-
'question' => '#96CEB4',
-
'announcement' => '#FECA57'
-
}
-
colors[category] || '#95A5A6'
-
end
-
-
# 辅助方法:标签颜色
-
def tag_color(tag)
-
# 简单的标签颜色生成算法
-
hash = Digest::MD5.hexdigest(tag)[0..5]
-
"##{hash}"
-
end
-
-
# 辅助方法:标签帖子数量
-
def tag_post_count(tag)
-
# 这里可以添加缓存逻辑
-
Post.where('tags LIKE ?', "%#{tag}%").count
-
end
-
-
# 辅助方法:缩略图URL
-
def thumbnail_url(image_url)
-
# 这里可以添加缩略图生成逻辑
-
image_url
-
end
-
-
# 辅助方法:图片宽度
-
def image_width(image_url)
-
# 这里可以添加图片尺寸获取逻辑
-
800
-
end
-
-
# 辅助方法:图片高度
-
def image_height(image_url)
-
# 这里可以添加图片尺寸获取逻辑
-
600
-
end
-
-
# 辅助方法:帖子URL
-
def post_url(post)
-
"/posts/#{post.id}"
-
end
-
-
private
-
-
# 使用预获取权限的帖子格式化方法
-
def format_post_with_permissions(post, current_user: nil, options: {}, permissions: {})
-
data = {
-
id: post.id,
-
title: post.title,
-
content: formatted_content,
-
summary: generate_summary,
-
stats: generate_stats,
-
time_data: generate_time_data,
-
author: generate_author_info,
-
category: generate_category_info,
-
tags: generate_tags_info,
-
images: generate_images_info,
-
interactions: generate_interaction_states_with_permissions(post, current_user, permissions),
-
metadata: generate_metadata
-
}
-
-
# 根据选项添加额外字段
-
data.merge!(add_optional_fields)
-
-
data
-
end
-
-
# 生成带预获取权限的交互状态
-
def generate_interaction_states_with_permissions(post, current_user, permissions)
-
return {} unless current_user && post
-
-
post_id = post.id
-
-
# 使用预获取的权限信息,如果没有则回退到原有方法
-
{
-
liked: post.liked_by?(current_user),
-
bookmarked: post.bookmarked_by?(current_user),
-
can_edit: permissions.dig(:edit, post_id) || PostPermissionService.can_edit?(post, current_user),
-
can_delete: permissions.dig(:delete, post_id) || PostPermissionService.can_delete?(post, current_user),
-
can_pin: permissions.dig(:pin, post_id) || PostPermissionService.can_pin?(post, current_user),
-
can_hide: permissions.dig(:hide, post_id) || PostPermissionService.can_hide?(post, current_user),
-
can_comment: permissions.dig(:comment, post_id) || PostPermissionService.can_comment?(post, current_user)
-
}
-
end
-
-
# 批量获取点赞状态
-
def self.batch_get_like_statuses(posts, current_user)
-
return {} unless posts.any? && current_user
-
-
post_ids = posts.map(&:id)
-
-
# 假设有Like模型,批量查询用户对帖子的点赞状态
-
if defined?(Like)
-
likes = Like.where(user: current_user, target_id: post_ids, target_type: 'Post')
-
likes.index_by(&:target_id).transform_values { true }
-
else
-
{}
-
end
-
rescue
-
{}
-
end
-
-
# 批量获取收藏状态
-
def self.batch_get_bookmark_statuses(posts, current_user)
-
return {} unless posts.any? && current_user
-
-
post_ids = posts.map(&:id)
-
-
# 假设有Bookmark模型,批量查询用户对帖子的收藏状态
-
# 如果没有Bookmark模型,返回空哈希
-
{}
-
rescue
-
{}
-
end
-
-
# 类方法:快速格式化
-
def self.format_post(post, current_user: nil, options: {})
-
service = new(post: post, current_user: current_user, options: options)
-
service.call
-
service.instance_variable_get(:@data)
-
end
-
-
# 类方法:批量格式化
-
def self.format_posts(posts, current_user: nil, options = {})
-
posts.map do |post|
-
format_post(post, current_user: current_user, options: options)
-
end
-
end
-
-
# 批量格式化帖子 - 优化列表页面性能
-
def self.batch_format_posts(posts, current_user: nil, options = {})
-
return [] if posts.blank?
-
-
# 预加载关联数据避免N+1查询
-
posts = posts.includes(:user, :tags, :likes) if posts.respond_to?(:includes)
-
-
# 如果有当前用户,批量获取权限信息
-
permissions = {}
-
if current_user && posts.any?
-
post_ids = posts.map(&:id)
-
permissions = PostPermissionService.batch_check_posts_permissions(
-
post_ids,
-
current_user.id,
-
[:edit, :delete, :pin, :hide, :comment]
-
)
-
end
-
-
# 批量处理帖子数据
-
posts.map do |post|
-
format_post_with_permissions(post, current_user: current_user, options: options, permissions: permissions)
-
end
-
end
-
-
# 批量生成帖子交互状态 - 权限优化版本
-
def self.batch_generate_interaction_states(posts, current_user)
-
return {} unless current_user && posts.any?
-
-
post_ids = posts.map(&:id)
-
-
# 批量获取权限信息
-
permissions = PostPermissionService.batch_check_posts_permissions(
-
post_ids,
-
current_user.id,
-
[:edit, :delete, :pin, :hide, :comment]
-
)
-
-
# 批量获取点赞和收藏状态
-
post_like_statuses = batch_get_like_statuses(posts, current_user)
-
post_bookmark_statuses = batch_get_bookmark_statuses(posts, current_user)
-
-
posts.map do |post|
-
post_id = post.id
-
{
-
post_id: post_id,
-
liked: post_like_statuses[post_id] || false,
-
bookmarked: post_bookmark_statuses[post_id] || false,
-
can_edit: permissions.dig(:edit, post_id) || false,
-
can_delete: permissions.dig(:delete, post_id) || false,
-
can_pin: permissions.dig(:pin, post_id) || false,
-
can_hide: permissions.dig(:hide, post_id) || false,
-
can_comment: permissions.dig(:comment, post_id) || false
-
}
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# PostManagementService - 帖子管理服务(重构版)
-
# 作为帖子相关服务的协调器,提供统一的接口
-
class PostManagementService < ApplicationService
-
include ServiceInterface
-
attr_reader :post, :user, :action, :params
-
-
def initialize(post: nil, user:, action:, params: {})
-
super()
-
@post = post
-
@user = user
-
@action = action
-
@params = params
-
end
-
-
# 主要调用方法
-
def call
-
handle_errors do
-
validate_params
-
execute_action
-
end
-
self
-
end
-
-
# 类方法:创建帖子
-
def self.create_post!(user, params)
-
new(user: user, action: :create, params: params).call
-
end
-
-
# 类方法:更新帖子
-
def self.update_post!(post, user, params)
-
new(post: post, user: user, action: :update, params: params).call
-
end
-
-
# 类方法:删除帖子
-
def self.delete_post!(post, user)
-
new(post: post, user: user, action: :delete).call
-
end
-
-
# 类方法:置顶帖子
-
def self.pin_post!(post, user)
-
new(post: post, user: user, action: :pin).call
-
end
-
-
# 类方法:取消置顶帖子
-
def self.unpin_post!(post, user)
-
new(post: post, user: user, action: :unpin).call
-
end
-
-
# 类方法:隐藏帖子
-
def self.hide_post!(post, user, reason: nil)
-
new(post: post, user: user, action: :hide, params: { reason: reason }).call
-
end
-
-
# 类方法:显示帖子
-
def self.unhide_post!(post, user)
-
new(post: post, user: user, action: :unhide).call
-
end
-
-
# 类方法:获取帖子数据
-
def self.get_post_data(post, current_user: nil, options: {})
-
PostDataService.format_post(post, current_user: current_user, options: options)
-
end
-
-
# 类方法:检查权限
-
def self.check_permission(post, user, action)
-
PostPermissionService.can_perform?(post, user, action)
-
end
-
-
private
-
-
# 验证参数
-
def validate_params
-
return failure!("用户不能为空") unless user
-
return failure!("用户不存在") unless user.persisted?
-
-
case action
-
when :create
-
return failure!("创建参数不能为空") if params.blank?
-
when :update, :delete, :pin, :unpin, :hide, :unhide
-
return failure!("帖子不能为空") unless post
-
return failure!("帖子不存在") unless post.persisted?
-
else
-
return failure!("不支持的操作: #{action}")
-
end
-
-
true
-
end
-
-
# 执行具体操作
-
def execute_action
-
result = case action
-
when :create
-
create_post
-
when :update
-
update_post
-
when :delete
-
delete_post
-
when :pin
-
moderate_post(:pin)
-
when :unpin
-
moderate_post(:unpin)
-
when :hide
-
moderate_post(:hide, reason: params[:reason])
-
when :unhide
-
moderate_post(:unhide)
-
else
-
failure!("不支持的操作: #{action}")
-
end
-
-
if result&.success?
-
# 从子服务的结果中获取数据
-
service_data = result.instance_variable_get(:@data)
-
success!(service_data)
-
else
-
failure!(result&.errors || ["操作失败"])
-
end
-
end
-
-
# 创建帖子
-
def create_post
-
PostCreationService.new(user: user, post_params: params).call
-
end
-
-
# 更新帖子
-
def update_post
-
PostUpdateService.new(post: post, user: user, post_params: params).call
-
end
-
-
# 删除帖子
-
def delete_post
-
PostModerationService.new(post: post, user: user, action: :delete).call
-
end
-
-
# 管理操作(置顶、隐藏等)
-
def moderate_post(moderation_action, reason: nil)
-
PostModerationService.new(
-
post: post,
-
user: user,
-
action: moderation_action,
-
reason: reason
-
).call
-
end
-
end
-
# frozen_string_literal: true
-
-
# PostModerationService - 帖子管理服务
-
# 专门负责帖子的管理操作,包括置顶、隐藏、删除等
-
class PostModerationService < ApplicationService
-
include ServiceInterface
-
attr_reader :post, :user, :action, :reason
-
-
def initialize(post:, user:, action:, reason: nil)
-
super()
-
@post = post
-
@user = user
-
@action = action
-
@reason = reason
-
end
-
-
# 执行管理操作
-
def call
-
handle_errors do
-
validate_moderation_params
-
check_moderation_permission
-
execute_moderation_action
-
process_moderation_result
-
format_success_response
-
end
-
self
-
end
-
-
private
-
-
# 验证管理参数
-
def validate_moderation_params
-
return failure!("帖子不能为空") unless post
-
return failure!("用户不能为空") unless user
-
return failure!("帖子不存在") unless post.persisted?
-
return failure!("用户不存在") unless user.persisted?
-
-
valid_actions = [:pin, :unpin, :hide, :unhide, :delete]
-
unless valid_actions.include?(action)
-
return failure!("不支持的管理操作: #{action}")
-
end
-
end
-
-
# 检查管理权限
-
def check_moderation_permission
-
case action
-
when :pin, :unpin
-
unless post.can_pin?(user)
-
failure!("无权限置顶此帖子")
-
return false
-
end
-
when :hide, :unhide
-
unless post.can_hide?(user)
-
failure!("无权限隐藏此帖子")
-
return false
-
end
-
when :delete
-
unless post.can_edit?(user)
-
failure!("无权限删除此帖子")
-
return false
-
end
-
end
-
-
true
-
end
-
-
# 执行管理操作
-
def execute_moderation_action
-
case action
-
when :pin
-
post.pin!
-
when :unpin
-
post.unpin!
-
when :hide
-
post.hide!
-
when :unhide
-
post.unhide!
-
when :delete
-
post.destroy!
-
end
-
-
true
-
rescue => e
-
Rails.logger.error "Post moderation error: #{e.message}"
-
failure!("管理操作失败: #{e.message}")
-
false
-
end
-
-
# 处理管理操作结果
-
def process_moderation_result
-
# 记录管理操作日志
-
log_moderation_event
-
-
# 发送相关通知
-
send_moderation_notifications
-
-
# 更新统计信息(如果需要)
-
update_statistics
-
-
# 清理缓存
-
clear_cache
-
end
-
-
# 记录管理操作日志
-
def log_moderation_event
-
action_text = case action
-
when :pin then "置顶"
-
when :unpin then "取消置顶"
-
when :hide then "隐藏"
-
when :unhide then "显示"
-
when :delete then "删除"
-
end
-
-
log_message = "Post #{action_text}: ID #{post.id} by User #{user.id}"
-
log_message += " - Reason: #{reason}" if reason.present?
-
-
Rails.logger.info log_message
-
end
-
-
# 发送管理操作通知
-
def send_moderation_notifications
-
case action
-
when :pin
-
# 通知帖子作者帖子被置顶
-
send_pin_notification
-
when :hide
-
# 通知帖子作者帖子被隐藏
-
send_hide_notification
-
when :delete
-
# 通知帖子作者帖子被删除
-
send_delete_notification
-
end
-
end
-
-
# 发送置顶通知
-
def send_pin_notification
-
return if user.id == post.user_id # 自己操作自己不通知
-
-
# NotificationService.post_pinned_notification(post, user)
-
end
-
-
# 发送隐藏通知
-
def send_hide_notification
-
return if user.id == post.user_id
-
-
# NotificationService.post_hidden_notification(post, user, reason)
-
end
-
-
# 发送删除通知
-
def send_delete_notification
-
return if user.id == post.user_id
-
-
# NotificationService.post_deleted_notification(post, user, reason)
-
end
-
-
# 更新统计信息
-
def update_statistics
-
case action
-
when :pin
-
# 更新置顶帖子统计
-
update_pin_statistics
-
when :hide
-
# 更新隐藏帖子统计
-
update_hide_statistics
-
when :delete
-
# 更新删除统计
-
update_delete_statistics
-
end
-
end
-
-
# 更新置顶统计
-
def update_pin_statistics
-
# 统计逻辑 - 检查字段是否存在
-
if post.user.respond_to?(:pinned_posts_count)
-
if action == :pin
-
post.user.increment!(:pinned_posts_count)
-
else
-
post.user.decrement!(:pinned_posts_count)
-
end
-
end
-
end
-
-
# 更新隐藏统计
-
def update_hide_statistics
-
# 统计逻辑
-
end
-
-
# 更新删除统计
-
def update_delete_statistics
-
# 更新用户帖子数量 - 检查字段是否存在
-
post.user.decrement!(:posts_count) if post.user.respond_to?(:posts_count)
-
end
-
-
# 清理缓存
-
def clear_cache
-
# 清理帖子相关的缓存
-
Rails.cache.delete("post_#{post.id}")
-
Rails.cache.delete("user_posts_#{post.user_id}")
-
Rails.cache.delete("posts_list")
-
end
-
-
# 格式化成功响应
-
def format_success_response
-
action_text = case action
-
when :pin then "置顶"
-
when :unpin then "取消置顶"
-
when :hide then "隐藏"
-
when :unhide then "显示"
-
when :delete then "删除"
-
end
-
-
response_data = {
-
message: "帖子#{action_text}成功"
-
}
-
-
# 对于非删除操作,返回更新后的帖子数据
-
unless action == :delete
-
response_data[:post] = post_data(post)
-
end
-
-
success!(response_data)
-
end
-
-
# 格式化帖子数据
-
def post_data(post)
-
return nil if action == :delete
-
post.as_json_for_api(current_user: user)
-
end
-
end
-
# frozen_string_literal: true
-
-
# PostPermissionService - 帖子权限检查服务
-
# 专门负责帖子相关操作的权限验证逻辑
-
class PostPermissionService < ApplicationService
-
include ServiceInterface
-
attr_reader :post, :user, :action
-
-
def initialize(post:, user:, action:)
-
super()
-
@post = post
-
@user = user
-
@action = action
-
end
-
-
# 检查权限
-
def call
-
handle_errors do
-
validate_permission_params
-
check_specific_permission
-
end
-
self
-
end
-
-
# 快速权限检查方法(不使用handle_errors包装)
-
def can_perform?
-
validate_permission_params && check_specific_permission
-
rescue
-
false
-
end
-
-
private
-
-
# 验证权限检查参数
-
def validate_permission_params
-
return failure!("用户不能为空") unless user
-
return failure!("帖子不能为空") unless post
-
return failure!("用户不存在") unless user.persisted?
-
return failure!("帖子不存在") unless post.persisted?
-
-
valid_actions = [:edit, :delete, :pin, :hide, :view, :comment]
-
unless valid_actions.include?(action)
-
return failure!("不支持的权限检查操作: #{action}")
-
end
-
-
true
-
end
-
-
# 检查具体权限
-
def check_specific_permission
-
result = case action
-
when :edit
-
can_edit?
-
when :delete
-
can_delete?
-
when :pin
-
can_pin?
-
when :hide
-
can_hide?
-
when :view
-
can_view?
-
when :comment
-
can_comment?
-
else
-
false
-
end
-
-
if result
-
success!("权限检查通过")
-
else
-
failure!("权限不足")
-
end
-
end
-
-
# 检查编辑权限
-
def can_edit?
-
# 帖子作者可以编辑自己的帖子
-
return true if post.user_id == user.id
-
-
# 管理员可以编辑任何帖子
-
return true if user.admin?
-
-
# 超级管理员可以编辑任何帖子
-
return true if user.super_admin?
-
-
false
-
end
-
-
# 检查删除权限
-
def can_delete?
-
# 帖子作者可以删除自己的帖子
-
return true if post.user_id == user.id
-
-
# 管理员可以删除任何帖子
-
return true if user.admin?
-
-
# 超级管理员可以删除任何帖子
-
return true if user.super_admin?
-
-
false
-
end
-
-
# 检查置顶权限
-
def can_pin?
-
# 只有管理员和超级管理员可以置顶帖子
-
return true if user.admin?
-
return true if user.super_admin?
-
-
false
-
end
-
-
# 检查隐藏权限
-
def can_hide?
-
# 管理员和超级管理员可以隐藏帖子
-
return true if user.admin?
-
return true if user.super_admin?
-
-
false
-
end
-
-
# 检查查看权限
-
def can_view?
-
# 已删除的帖子只有作者和管理员可以查看
-
if post.deleted?
-
return post.user_id == user.id || user.admin? || user.super_admin?
-
end
-
-
# 隐藏的帖子只有作者和管理员可以查看
-
if post.hidden?
-
return post.user_id == user.id || user.admin? || user.super_admin?
-
end
-
-
# 公开帖子所有人都可以查看
-
true
-
end
-
-
# 检查评论权限
-
def can_comment?
-
# 不能对已删除的帖子评论
-
return false if post.deleted?
-
-
# 不能对隐藏的帖子评论(除非是作者或管理员)
-
if post.hidden?
-
return post.user_id == user.id || user.admin? || user.super_admin?
-
end
-
-
# 其他情况都可以评论
-
true
-
end
-
-
# 类方法:快速权限检查
-
def self.can_edit?(post, user)
-
new(post: post, user: user, action: :edit).can_perform?
-
end
-
-
def self.can_delete?(post, user)
-
new(post: post, user: user, action: :delete).can_perform?
-
end
-
-
def self.can_pin?(post, user)
-
new(post: post, user: user, action: :pin).can_perform?
-
end
-
-
def self.can_hide?(post, user)
-
new(post: post, user: user, action: :hide).can_perform?
-
end
-
-
def self.can_view?(post, user)
-
new(post: post, user: user, action: :view).can_perform?
-
end
-
-
def self.can_comment?(post, user)
-
new(post: post, user: user, action: :comment).can_perform?
-
end
-
-
# 带缓存的权限检查方法
-
def self.can_edit_cached?(post, user, cache_options = {})
-
can_perform_cached?(:edit, post, user, cache_options)
-
end
-
-
def self.can_delete_cached?(post, user, cache_options = {})
-
can_perform_cached?(:delete, post, user, cache_options)
-
end
-
-
def self.can_pin_cached?(post, user, cache_options = {})
-
can_perform_cached?(:pin, post, user, cache_options)
-
end
-
-
def self.can_hide_cached?(post, user, cache_options = {})
-
can_perform_cached?(:hide, post, user, cache_options)
-
end
-
-
def self.can_view_cached?(post, user, cache_options = {})
-
can_perform_cached?(:view, post, user, cache_options)
-
end
-
-
def self.can_comment_cached?(post, user, cache_options = {})
-
can_perform_cached?(:comment, post, user, cache_options)
-
end
-
-
# 批量权限检查 - 优化列表页面性能
-
def self.batch_check_posts_permissions(post_ids, user_id, actions = [:edit, :delete, :pin, :hide, :comment])
-
return {} if post_ids.blank? || user_id.blank?
-
-
cache_keys = actions.product(post_ids).map do |action, post_id|
-
"post_permission:#{action}:#{post_id}:#{user_id}"
-
end
-
-
# 尝试从缓存获取
-
cached_results = Rails.cache.read_multi(*cache_keys)
-
-
# 找出需要查询的权限
-
uncached_permissions = []
-
actions.each do |action|
-
post_ids.each do |post_id|
-
cache_key = "post_permission:#{action}:#{post_id}:#{user_id}"
-
unless cached_results.key?(cache_key)
-
uncached_permissions << { action: action, post_id: post_id, cache_key: cache_key }
-
end
-
end
-
end
-
-
# 批量查询并缓存未缓存的权限
-
if uncached_permissions.any?
-
batch_cache_permissions(uncached_permissions, user_id)
-
end
-
-
# 组织返回结果
-
results = {}
-
actions.each do |action|
-
results[action] = {}
-
post_ids.each do |post_id|
-
cache_key = "post_permission:#{action}:#{post_id}:#{user_id}"
-
results[action][post_id.to_i] = cached_results[cache_key] || false
-
end
-
end
-
-
results
-
end
-
-
private
-
-
# 通用缓存权限检查方法
-
def self.can_perform_cached?(action, post, user, cache_options = {})
-
return false unless post&.persisted? && user&.persisted?
-
-
cache_key = "post_permission:#{action}:#{post.id}:#{user.id}"
-
cache_options = {
-
expires_in: cache_options[:expires_in] || 5.minutes,
-
race_condition_ttl: 10.seconds
-
}.merge(cache_options)
-
-
Rails.cache.fetch(cache_key, cache_options) do
-
new(post: post, user: user, action: action).can_perform?
-
end
-
end
-
-
# 批量缓存权限检查结果
-
def self.batch_cache_permissions(permissions, user_id)
-
# 按action分组以优化数据库查询
-
permissions_by_action = permissions.group_by { |p| p[:action] }
-
-
permissions_by_action.each do |action, perms|
-
post_ids = perms.map { |p| p[:post_id] }
-
-
# 批量加载帖子和用户
-
posts = Post.where(id: post_ids).includes(:user)
-
user = User.find_by(id: user_id)
-
next unless user
-
-
# 批量检查权限
-
perms.each do |perm|
-
post = posts.find { |p| p.id == perm[:post_id] }
-
next unless post
-
-
result = new(post: post, user: user, action: action).can_perform?
-
Rails.cache.write(perm[:cache_key], result, expires_in: 5.minutes)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# PostServiceFacade - 帖子服务门面
-
# 简化控制器层对帖子服务的调用,提供统一的接口
-
class PostServiceFacade < ApplicationService
-
include ServiceInterface
-
-
attr_reader :user, :current_user, :params
-
-
def initialize(user:, current_user: nil, params: {})
-
super()
-
@user = user
-
@current_user = current_user || user
-
@params = params
-
end
-
-
def call
-
handle_errors do
-
validate_parameters
-
execute_operation
-
end
-
self
-
end
-
-
# 类方法:创建帖子并返回格式化数据
-
def self.create_with_data(user, params, current_user: nil)
-
new(user: user, current_user: current_user, params: params).tap do |facade|
-
facade.instance_variable_set(:@action, :create)
-
facade.call
-
end
-
end
-
-
# 类方法:更新帖子并返回格式化数据
-
def self.update_with_data(post, user, params, current_user: nil)
-
facade = new(user: user, current_user: current_user, params: params)
-
facade.instance_variable_set(:@post, post)
-
facade.instance_variable_set(:@action, :update)
-
facade.call
-
facade
-
end
-
-
# 类方法:删除帖子
-
def self.delete_post(post, user, current_user: nil)
-
facade = new(user: user, current_user: current_user)
-
facade.instance_variable_set(:@post, post)
-
facade.instance_variable_set(:@action, :delete)
-
facade.call
-
facade
-
end
-
-
# 类方法:置顶帖子
-
def self.pin_post(post, user, current_user: nil)
-
facade = new(user: user, current_user: current_user)
-
facade.instance_variable_set(:@post, post)
-
facade.instance_variable_set(:@action, :pin)
-
facade.call
-
facade
-
end
-
-
# 类方法:取消置顶帖子
-
def self.unpin_post(post, user, current_user: nil)
-
facade = new(user: user, current_user: current_user)
-
facade.instance_variable_set(:@post, post)
-
facade.instance_variable_set(:@action, :unpin)
-
facade.call
-
facade
-
end
-
-
private
-
-
def validate_parameters
-
case action
-
when :create
-
errors.add(:user, "用户不能为空") if user.blank?
-
errors.add(:params, "创建参数不能为空") if params.blank?
-
errors.add(:title, "标题不能为空") if params[:title].blank?
-
errors.add(:content, "内容不能为空") if params[:content].blank?
-
when :update, :delete, :pin, :unpin
-
post = instance_variable_get(:@post)
-
errors.add(:post, "帖子不能为空") if post.blank?
-
errors.add(:user, "用户不能为空") if user.blank?
-
end
-
end
-
-
def execute_operation
-
case action
-
when :create
-
create_post_with_data
-
when :update
-
update_post_with_data
-
when :delete
-
delete_post_action
-
when :pin, :unpin
-
moderate_post_action(action)
-
else
-
failure!("不支持的操作")
-
end
-
end
-
-
def create_post_with_data
-
# 使用PostCreationService创建帖子
-
creation_result = PostCreationService.new(user: user, post_params: params).call
-
-
unless creation_result.success?
-
return failure!(creation_result.error_messages)
-
end
-
-
post = creation_result.data[:post]
-
-
# 发布帖子创建事件
-
DomainEventsService.publish('post.created', {
-
post: post,
-
user: user
-
})
-
-
# 格式化帖子数据
-
formatted_data = PostDataService.format_post(post, current_user: current_user)
-
-
success!({
-
post: formatted_data,
-
message: "帖子创建成功"
-
})
-
end
-
-
def update_post_with_data
-
post = instance_variable_get(:@post)
-
-
# 使用PostUpdateService更新帖子
-
update_result = PostUpdateService.new(
-
post: post,
-
user: user,
-
post_params: params
-
).call
-
-
unless update_result.success?
-
return failure!(update_result.error_messages)
-
end
-
-
# 发布帖子更新事件
-
DomainEventsService.publish('post.updated', {
-
post: post,
-
user: user
-
})
-
-
# 格式化帖子数据
-
formatted_data = PostDataService.format_post(post, current_user: current_user)
-
-
success!({
-
post: formatted_data,
-
message: "帖子更新成功"
-
})
-
end
-
-
def delete_post_action
-
post = instance_variable_get(:@post)
-
-
# 使用PostModerationService删除帖子
-
deletion_result = PostModerationService.new(
-
post: post,
-
user: user,
-
action: :delete
-
).call
-
-
if deletion_result.success?
-
# 发布帖子审核事件
-
DomainEventsService.publish('post.moderated', {
-
post: post,
-
moderator: user,
-
action: :delete,
-
reason: params[:reason]
-
})
-
-
success!({ message: "帖子删除成功" })
-
else
-
failure!(deletion_result.error_messages)
-
end
-
end
-
-
def moderate_post_action(moderation_action)
-
post = instance_variable_get(:@post)
-
-
# 使用PostModerationService进行管理操作
-
moderation_result = PostModerationService.new(
-
post: post,
-
user: user,
-
action: moderation_action,
-
reason: params[:reason]
-
).call
-
-
if moderation_result.success?
-
# 发布帖子审核事件
-
DomainEventsService.publish('post.moderated', {
-
post: post,
-
moderator: user,
-
action: moderation_action,
-
reason: params[:reason]
-
})
-
-
action_name = moderation_action == :pin ? "置顶" : "取消置顶"
-
success!({ message: "帖子#{action_name}成功" })
-
else
-
failure!(moderation_result.error_messages)
-
end
-
end
-
-
def action
-
@action || :create
-
end
-
end
-
# frozen_string_literal: true
-
-
# PostUpdateService - 帖子更新服务
-
# 专门负责帖子的更新逻辑,包括权限验证、内容更新等
-
class PostUpdateService < ApplicationService
-
include ServiceInterface
-
attr_reader :post, :user, :post_params
-
-
def initialize(post:, user:, post_params:)
-
super()
-
@post = post
-
@user = user
-
@post_params = post_params
-
end
-
-
# 更新帖子
-
def call
-
handle_errors do
-
validate_update_params
-
check_edit_permission
-
update_post
-
process_post_update
-
format_success_response
-
end
-
self
-
end
-
-
private
-
-
# 验证更新参数
-
def validate_update_params
-
return failure!("帖子不能为空") unless post
-
return failure!("用户不能为空") unless user
-
return failure!("帖子不存在") unless post.persisted?
-
return failure!("用户不存在") unless user.persisted?
-
-
# 验证内容长度(如果提供)
-
if post_params[:content].present?
-
if post_params[:content].length < 10
-
return failure!("内容长度不能少于10个字符")
-
end
-
-
if post_params[:content].length > 10000
-
return failure!("内容长度不能超过10000个字符")
-
end
-
end
-
-
# 验证标题长度(如果提供)
-
if post_params[:title].present?
-
if post_params[:title].length > 100
-
return failure!("标题长度不能超过100个字符")
-
end
-
end
-
end
-
-
# 检查编辑权限
-
def check_edit_permission
-
unless post.can_edit?(user)
-
failure!("无权限编辑此帖子")
-
return false
-
end
-
-
true
-
end
-
-
# 更新帖子
-
def update_post
-
unless post.update(post_params)
-
failure!(post.errors.full_messages)
-
return false
-
end
-
-
true
-
end
-
-
# 处理帖子更新后的逻辑
-
def process_post_update
-
# 处理标签更新
-
process_tags_update if post_params[:tags].present?
-
-
# 处理图片更新
-
process_images_update if post_params[:images].present?
-
-
# 记录更新日志
-
log_update_event
-
-
# 发送更新通知(如果需要)
-
send_update_notifications
-
end
-
-
# 处理标签更新
-
def process_tags_update
-
tags = post_params[:tags].map(&:strip).reject(&:blank?).uniq
-
post.update!(tags: tags)
-
end
-
-
# 处理图片更新
-
def process_images_update
-
valid_images = post_params[:images].select { |url| valid_image_url?(url) }
-
post.update!(images: valid_images)
-
end
-
-
# 验证图片URL
-
def valid_image_url?(url)
-
uri = URI.parse(url)
-
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
-
rescue URI::InvalidURIError
-
false
-
end
-
-
# 记录更新事件
-
def log_update_event
-
Rails.logger.info "Post updated: ID #{post.id} by User #{user.id}"
-
end
-
-
# 发送更新通知
-
def send_update_notifications
-
# 这里可以添加通知逻辑,比如通知关注者有更新
-
# NotificationService.post_updated_notification(post)
-
end
-
-
# 格式化成功响应
-
def format_success_response
-
success!({
-
message: "帖子更新成功",
-
post: post_data(post)
-
})
-
end
-
-
# 格式化帖子数据
-
def post_data(post)
-
post.as_json_for_api(current_user: user)
-
end
-
end
-
# frozen_string_literal: true
-
-
# QueryCacheService - 查询缓存服务
-
# 提供多层缓存策略:内存缓存、Redis缓存、查询结果缓存
-
class QueryCacheService < ApplicationService
-
include ServiceInterface
-
-
# 缓存层级
-
MEMORY_CACHE = {}
-
CACHE_LOCKS = {}
-
-
attr_reader :cache_key, :cache_options, :fallback_proc, :cache_level
-
-
def initialize(cache_key:, cache_options: {}, fallback_proc: nil, cache_level: :redis)
-
super()
-
@cache_key = cache_key
-
@cache_options = default_cache_options.merge(cache_options)
-
@fallback_proc = fallback_proc
-
@cache_level = cache_level
-
end
-
-
def call
-
handle_errors do
-
validate_parameters
-
fetch_with_cache
-
end
-
self
-
end
-
-
# 类方法:缓存查询结果
-
def self.fetch(cache_key, cache_options: {}, cache_level: :redis, &block)
-
new(
-
cache_key: cache_key,
-
cache_options: cache_options,
-
fallback_proc: block,
-
cache_level: cache_level
-
).call.data
-
end
-
-
# 类方法:缓存帖子列表
-
def self.fetch_posts_list(filters = {}, page: 1, per_page: 20, current_user: nil)
-
cache_key = "posts_list:#{filters.to_query_hash}:#{page}:#{per_page}:#{current_user&.id}"
-
-
fetch(cache_key,
-
expires_in: 5.minutes,
-
cache_level: :redis) do
-
# 构建查询
-
posts = Post.visible.includes(:user)
-
.order(pinned: :desc, created_at: :desc)
-
-
# 应用筛选条件
-
posts = posts.by_category(filters[:category]) if filters[:category].present?
-
-
# 分页
-
posts = posts.limit(per_page).offset((page - 1) * per_page)
-
-
# 预加载权限和点赞状态
-
if current_user
-
post_ids = posts.map(&:id)
-
permissions = PostPermissionService.batch_check_posts_permissions(
-
post_ids, current_user.id
-
)
-
liked_post_ids = Like.where(
-
user_id: current_user.id,
-
target_type: 'Post',
-
target_id: post_ids
-
).pluck(:target_id)
-
-
posts.each do |post|
-
post.instance_variable_set(:@permissions, permissions)
-
post.instance_variable_set(:@current_user_liked, liked_post_ids.include?(post.id))
-
end
-
end
-
-
posts
-
end
-
end
-
-
# 类方法:缓存单个帖子
-
def self.fetch_post(post_id, current_user: nil)
-
cache_key = "post:#{post_id}:#{current_user&.id}"
-
-
fetch(cache_key,
-
expires_in: 10.minutes,
-
cache_level: :redis) do
-
post = Post.includes(:user).find(post_id)
-
-
# 预加载权限和点赞状态
-
if current_user
-
permissions = PostPermissionService.batch_check_posts_permissions(
-
[post_id], current_user.id
-
)
-
liked = Like.exists?(
-
user_id: current_user.id,
-
target_type: 'Post',
-
target_id: post_id
-
)
-
-
post.instance_variable_set(:@permissions, permissions)
-
post.instance_variable_set(:@current_user_liked, liked)
-
end
-
-
post
-
end
-
end
-
-
# 类方法:缓存用户统计
-
def self.fetch_user_stats(user_id)
-
cache_key = "user_stats:#{user_id}"
-
-
fetch(cache_key,
-
expires_in: 1.hour,
-
cache_level: :redis) do
-
user = User.find(user_id)
-
-
{
-
posts_count: user.posts_count,
-
comments_count: user.comments_count,
-
flowers_given_count: user.flowers_given_count,
-
flowers_received_count: user.flowers_received_count,
-
likes_given_count: user.likes_given_count
-
}
-
end
-
end
-
-
# 类方法:缓存活动统计
-
def self.fetch_event_stats(event_id)
-
cache_key = "event_stats:#{event_id}"
-
-
fetch(cache_key,
-
expires_in: 30.minutes,
-
cache_level: :redis) do
-
event = ReadingEvent.find(event_id)
-
-
{
-
enrollments_count: event.enrollments_count,
-
check_ins_count: event.check_ins_count,
-
flowers_count: event.flowers_count,
-
completion_rate: calculate_completion_rate(event)
-
}
-
end
-
end
-
-
# 类方法:清除缓存
-
def self.clear_cache(pattern = nil)
-
if pattern
-
# 清除匹配模式的缓存
-
if defined?(Rails) && Rails.cache.respond_to?(:delete_matched)
-
Rails.cache.delete_matched(pattern)
-
end
-
-
# 清除内存缓存
-
MEMORY_CACHE.delete_if { |key, _| key.match?(Regexp.new(pattern)) }
-
else
-
# 清除所有缓存
-
if defined?(Rails) && Rails.cache.respond_to?(:clear)
-
Rails.cache.clear
-
end
-
-
MEMORY_CACHE.clear
-
end
-
end
-
-
# 类方法:预热缓存
-
def self.warmup_popular_data
-
# 预热热门帖子
-
popular_posts = Post.visible.order(likes_count: :desc).limit(10)
-
popular_posts.each do |post|
-
fetch_post(post.id)
-
end
-
-
# 预热活动统计
-
active_events = ReadingEvent.where(status: :active).limit(5)
-
active_events.each do |event|
-
fetch_event_stats(event.id)
-
end
-
-
Rails.logger.info "缓存预热完成"
-
end
-
-
def data
-
@data
-
end
-
-
def cache_hit?
-
@cache_hit
-
end
-
-
private
-
-
def validate_parameters
-
errors.add(:cache_key, "缓存键不能为空") if cache_key.blank?
-
errors.add(:fallback_proc, "必须提供fallback_proc或代码块") if fallback_proc.nil?
-
end
-
-
def fetch_with_cache
-
# 尝试从缓存获取
-
cached_value = get_from_cache
-
-
if cached_value.present?
-
@cache_hit = true
-
@data = cached_value
-
Rails.logger.debug "缓存命中: #{cache_key}"
-
return self
-
end
-
-
# 防止缓存击穿
-
@cache_hit = false
-
@data = fetch_with_lock
-
-
# 存入缓存
-
set_to_cache(@data)
-
-
Rails.logger.debug "缓存未命中,已设置: #{cache_key}"
-
self
-
end
-
-
def fetch_with_lock
-
# 使用分布式锁防止缓存击穿
-
lock_key = "cache_lock:#{cache_key}"
-
-
if cache_level == :redis && defined?(Rails)
-
# 使用Redis分布式锁
-
lock_value = SecureRandom.uuid
-
-
if Rails.cache.add(lock_key, lock_value, expires_in: 30.seconds)
-
begin
-
result = fallback_proc.call
-
return result
-
ensure
-
Rails.cache.delete(lock_key)
-
end
-
else
-
# 等待其他进程完成,然后重试获取缓存
-
sleep(0.1)
-
cached_value = get_from_cache
-
return cached_value if cached_value.present?
-
end
-
else
-
# 使用内存锁
-
CACHE_LOCKS[cache_key] ||= Mutex.new
-
CACHE_LOCKS[cache_key].synchronize do
-
result = fallback_proc.call
-
return result
-
end
-
end
-
end
-
-
def get_from_cache
-
case cache_level
-
when :memory
-
MEMORY_CACHE[cache_key]
-
when :redis
-
if defined?(Rails) && Rails.cache
-
Rails.cache.read(cache_key)
-
else
-
nil
-
end
-
else
-
nil
-
end
-
end
-
-
def set_to_cache(value)
-
return unless value
-
-
case cache_level
-
when :memory
-
MEMORY_CACHE[cache_key] = value
-
when :redis
-
if defined?(Rails) && Rails.cache
-
Rails.cache.write(cache_key, value, **cache_options)
-
end
-
end
-
end
-
-
def default_cache_options
-
{
-
expires_in: 30.minutes,
-
race_condition_ttl: 30.seconds,
-
compress: true
-
}
-
end
-
-
def calculate_completion_rate(event)
-
return 0 if event.enrollments_count == 0
-
-
total_days = (event.end_date - event.start_date).to_i + 1
-
expected_check_ins = event.enrollments_count * total_days
-
-
return 0 if expected_check_ins == 0
-
-
(event.check_ins_count.to_f / expected_check_ins * 100).round(2)
-
end
-
end
-
# frozen_string_literal: true
-
-
# 查询优化服务
-
# 提供高性能的数据库查询方法,减少N+1查询和优化复杂查询
-
class QueryOptimizationService
-
class << self
-
# 批量预加载关联数据,避免N+1查询
-
# @param records [Array] ActiveRecord记录数组
-
# @param includes [Array] 需要预加载的关联
-
# @return [Array] 预加载后的记录
-
def preload_associations(records, includes)
-
return records if records.empty?
-
-
# 使用ActiveRecord的preload方法避免N+1查询
-
if records.first.is_a?(Class)
-
# 如果是模型类,使用includes
-
records.includes(includes)
-
else
-
# 如果是记录数组,使用preload
-
ActiveRecord::Associations::Preloader.new.preload(records, includes)
-
records
-
end
-
end
-
-
# 优化的用户查询,包含常用关联
-
# @param scope [ActiveRecord::Relation] 基础查询范围
-
# @param options [Hash] 查询选项
-
# @return [ActiveRecord::Relation] 优化后的查询
-
def optimized_user_query(scope = User.all, options = {})
-
includes = [:created_events, :event_enrollments, :check_ins, :comments]
-
includes << :received_flowers if options[:include_flowers]
-
includes << :flower_certificates if options[:include_certificates]
-
-
scope.includes(includes)
-
end
-
-
# 优化的活动查询,包含统计信息
-
# @param scope [ActiveRecord::Relation] 基础查询范围
-
# @param options [Hash] 查询选项
-
# @return [ActiveRecord::Relation] 优化后的查询
-
def optimized_event_query(scope = ReadingEvent.all, options = {})
-
includes = [:leader, :event_enrollments, :reading_schedules]
-
includes << :check_ins if options[:include_check_ins]
-
includes << :flowers if options[:include_flowers]
-
-
query = scope.includes(includes)
-
-
# 如果需要统计数据,使用子查询而不是JOIN
-
if options[:include_stats]
-
query = query.select(
-
'reading_events.*',
-
'(SELECT COUNT(*) FROM event_enrollments WHERE event_enrollments.reading_event_id = reading_events.id AND event_enrollments.status = \'enrolled\') as enrolled_count',
-
'(SELECT COUNT(*) FROM check_ins JOIN reading_schedules ON check_ins.reading_schedule_id = reading_schedules.id WHERE reading_schedules.reading_event_id = reading_events.id) as check_ins_count',
-
'(SELECT COUNT(*) FROM flowers JOIN check_ins ON flowers.check_in_id = check_ins.id JOIN reading_schedules ON check_ins.reading_schedule_id = reading_schedules.id WHERE reading_schedules.reading_event_id = reading_events.id) as flowers_count'
-
)
-
end
-
-
query
-
end
-
-
# 优化的打卡查询,包含内容分析
-
# @param scope [ActiveRecord::Relation] 基础查询范围
-
# @param options [Hash] 查询选项
-
# @return [ActiveRecord::Relation] 优化后的查询
-
def optimized_check_in_query(scope = CheckIn.all, options = {})
-
includes = [:user, :reading_schedule, :enrollment]
-
includes << :flowers if options[:include_flowers]
-
includes << :comments if options[:include_comments]
-
includes << :reading_event if options[:include_event]
-
-
scope.includes(includes)
-
end
-
-
# 优化的通知查询,优先显示未读通知
-
# @param user [User] 用户对象
-
# @param options [Hash] 查询选项
-
# @return [ActiveRecord::Relation] 优化后的查询
-
def optimized_notification_query(user, options = {})
-
query = user.received_notifications
-
-
# 按未读状态和创建时间排序,未读通知优先
-
query = query.order(read: :asc, created_at: :desc)
-
-
# 包含关联数据
-
includes = [:actor]
-
includes << :notifiable if options[:include_notifiable]
-
query = query.includes(includes)
-
-
query
-
end
-
-
# 批量查询优化 - 使用IN查询而不是多次单独查询
-
# @param model_class [Class] ActiveRecord模型类
-
# @param ids [Array] ID数组
-
# @param includes [Array] 需要预加载的关联
-
# @return [Array] 查询结果
-
def batch_find_by_ids(model_class, ids, includes = [])
-
return [] if ids.empty?
-
-
# 分批处理,避免IN子句过长
-
batch_size = 1000
-
results = []
-
-
ids.each_slice(batch_size) do |batch_ids|
-
query = model_class.where(id: batch_ids)
-
query = query.includes(includes) if includes.any?
-
results.concat(query.to_a)
-
end
-
-
# 按原始ID顺序排序
-
id_index = ids.each_with_index.to_h
-
results.sort_by { |record| id_index[record.id] }
-
end
-
-
# 优化的排行榜查询,使用窗口函数提高性能
-
# @param model_class [Class] 模型类
-
# @param count_column [String] 计数字段名
-
# @param limit [Integer] 返回记录数限制
-
# @param includes [Array] 需要预加载的关联
-
# @return [Array] 排行榜数据
-
def optimized_leaderboard_query(model_class, count_column, limit = 10, includes = [])
-
# 使用窗口函数的子查询(如果数据库支持)
-
if database_supports_window_functions?
-
sql = <<~SQL
-
SELECT *,
-
DENSE_RANK() OVER (ORDER BY #{count_column} DESC, created_at ASC) as rank
-
FROM #{model_class.table_name}
-
ORDER BY #{count_column} DESC, created_at ASC
-
LIMIT ?
-
SQL
-
-
records = model_class.find_by_sql([sql, limit])
-
else
-
# 回退到普通查询
-
records = model_class.order("#{count_column} DESC, created_at ASC")
-
.limit(limit)
-
.to_a
-
-
# 手动计算排名
-
records.each_with_index do |record, index|
-
record.define_singleton_method(:rank) { index + 1 }
-
end
-
end
-
-
# 预加载关联数据
-
if includes.any?
-
ActiveRecord::Associations::Preloader.new.preload(records, includes)
-
end
-
-
records
-
end
-
-
# 优化的计数查询,使用缓存避免重复计算
-
# @param query [ActiveRecord::Relation] 查询对象
-
# @param cache_key [String] 缓存键
-
# @param cache_ttl [Integer] 缓存时间(秒)
-
# @return [Integer] 计数结果
-
def optimized_count_query(query, cache_key = nil, cache_ttl = 5.minutes)
-
if cache_key && Rails.cache.respond_to?(:fetch)
-
Rails.cache.fetch(cache_key, expires_in: cache_ttl) do
-
query.count
-
end
-
else
-
query.count
-
end
-
end
-
-
# 优化的存在性查询,使用EXISTS而不是COUNT
-
# @param query [ActiveRecord::Relation] 查询对象
-
# @return [Boolean] 是否存在记录
-
def optimized_exists_query(query)
-
query.exists?
-
end
-
-
# 优化的分页查询,使用cursor-based分页提高性能
-
# @param scope [ActiveRecord::Relation] 基础查询范围
-
# @param cursor [Integer] 游标位置
-
# @param limit [Integer] 每页记录数
-
# @param order_column [String] 排序字段
-
# @return [Array] 分页结果和下一页游标
-
def cursor_paginated_query(scope, cursor: nil, limit: 20, order_column: 'id')
-
query = scope.order(order_column => :asc).limit(limit + 1)
-
-
if cursor
-
query = query.where("#{order_column} > ?", cursor)
-
end
-
-
records = query.to_a
-
-
has_next = records.length > limit
-
next_cursor = has_next ? records.last.send(order_column) : nil
-
-
records = records.first(limit)
-
-
{
-
records: records,
-
next_cursor: next_cursor,
-
has_next: has_next
-
}
-
end
-
-
# 批量插入优化,使用批量插入减少数据库往返
-
# @param model_class [Class] 模型类
-
# @param attributes_array [Array] 属性数组
-
# @param batch_size [Integer] 批次大小
-
# @return [Array] 创建的记录
-
def batch_insert(model_class, attributes_array, batch_size = 1000)
-
return [] if attributes_array.empty?
-
-
created_records = []
-
-
attributes_array.each_slice(batch_size) do |batch|
-
records = model_class.insert_all(batch, returning: true)
-
created_records.concat(records)
-
end
-
-
created_records
-
end
-
-
# 优化的统计查询,使用数据库聚合函数
-
# @param model_class [Class] 模型类
-
# @param group_column [String] 分组字段
-
# @param aggregations [Hash] 聚合配置
-
# @return [Array] 统计结果
-
def optimized_aggregation_query(model_class, group_column, aggregations)
-
query = model_class.group(group_column)
-
-
aggregations.each do |alias_name, aggregation|
-
case aggregation[:type]
-
when :count
-
query = query.select("#{group_column}, COUNT(*) as #{alias_name}")
-
when :sum
-
query = query.select("#{group_column}, SUM(#{aggregation[:column]}) as #{alias_name}")
-
when :avg
-
query = query.select("#{group_column}, AVG(#{aggregation[:column]}) as #{alias_name}")
-
when :max
-
query = query.select("#{group_column}, MAX(#{aggregation[:column]}) as #{alias_name}")
-
when :min
-
query = query.select("#{group_column}, MIN(#{aggregation[:column]}) as #{alias_name}")
-
end
-
end
-
-
query.to_a
-
end
-
-
private
-
-
# 检查数据库是否支持窗口函数
-
def database_supports_window_functions?
-
case ActiveRecord::Base.connection.adapter_name.downcase
-
when 'postgresql', 'mysql'
-
true
-
when 'sqlite'
-
# SQLite 3.25+ 支持窗口函数
-
sqlite_version = ActiveRecord::Base.connection.select_value("SELECT sqlite_version()")
-
Gem::Version.new(sqlite_version) >= Gem::Version.new('3.25.0')
-
else
-
false
-
end
-
end
-
-
# 生成查询的缓存键
-
def generate_cache_key(model_class, query_params = {})
-
key_parts = [
-
model_class.name.downcase,
-
'query',
-
Digest::MD5.hexdigest(query_params.to_json)
-
]
-
key_parts.join('_')
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# ReportCreationService - 举报创建服务
-
# 专门负责内容举报的创建、验证和初步处理
-
class ReportCreationService < ApplicationService
-
include ServiceInterface
-
attr_reader :user, :target_content, :reason, :description
-
-
def initialize(user:, target_content:, reason:, description: nil)
-
super()
-
@user = user
-
@target_content = target_content
-
@reason = reason
-
@description = description
-
end
-
-
# 创建举报
-
def call
-
handle_errors do
-
validate_creation_params
-
check_creation_permissions
-
validate_report_reason
-
create_report_record
-
process_report_creation
-
format_success_response
-
end
-
self
-
end
-
-
private
-
-
# 验证创建参数
-
def validate_creation_params
-
return failure!("用户不能为空") unless user
-
return failure!("用户不存在") unless user.persisted?
-
return failure!("举报内容不能为空") unless target_content
-
return failure!("举报内容不存在") unless target_content.persisted?
-
return failure!("举报原因不能为空") unless reason
-
return failure!("无效的举报原因") unless valid_reason?
-
-
true
-
end
-
-
# 检查创建权限
-
def check_creation_permissions
-
# 检查是否可以举报此内容
-
unless can_report_content?
-
failure!("无权举报此内容或已举报过")
-
return false
-
end
-
-
true
-
end
-
-
# 验证举报原因
-
def validate_report_reason
-
issues = []
-
-
# 检查举报原因是否合理
-
if reason == 'other' && description.blank?
-
issues << '选择"其他"原因时必须填写描述'
-
end
-
-
# 检查描述长度
-
if description.present? && description.length < 10
-
issues << '举报描述太短,请提供更多详细信息'
-
end
-
-
# 检查描述长度上限
-
if description.present? && description.length > 500
-
issues << '举报描述不能超过500个字符'
-
end
-
-
# 检查内容是否确实有问题(对敏感词举报进行验证)
-
if reason == 'sensitive_words'
-
unless content_has_sensitive_words?
-
issues << '内容中未检测到敏感词,请确认举报原因'
-
end
-
end
-
-
if issues.any?
-
failure!("举报验证失败: #{issues.join(', ')}")
-
return false
-
end
-
-
true
-
end
-
-
# 创建举报记录
-
def create_report_record
-
@report = ContentReport.new(
-
user: user,
-
target_content: target_content,
-
reason: reason,
-
description: description,
-
status: :pending
-
)
-
-
unless @report.save
-
failure!("举报创建失败: #{@report.errors.full_messages.join(', ')}")
-
return false
-
end
-
-
true
-
end
-
-
# 处理举报创建后的逻辑
-
def process_report_creation
-
# 记录创建日志
-
log_report_creation
-
-
# 检查是否需要自动处理
-
check_auto_processing
-
-
# 异步通知管理员
-
schedule_admin_notification
-
end
-
-
# 格式化成功响应
-
def format_success_response
-
success!({
-
message: "举报提交成功",
-
report: report_data(@report),
-
auto_processed: @auto_processed || false
-
})
-
end
-
-
# 格式化举报数据
-
def report_data(report)
-
report.as_json_for_api(current_user: user)
-
end
-
-
# 检查是否可以举报内容
-
def can_report_content?
-
# 不能举报自己的内容
-
return false if target_content.user_id == user.id
-
-
# 检查是否已经举报过
-
return false if ContentReport.exists?(
-
user: user,
-
target_content: target_content,
-
status: [:pending, :approved]
-
)
-
-
true
-
end
-
-
# 验证举报原因是否有效
-
def valid_reason?
-
valid_reasons = %w[inappropriate_content spam sensitive_words harassment other]
-
valid_reasons.include?(reason.to_s)
-
end
-
-
# 检查内容是否包含敏感词
-
def content_has_sensitive_words?
-
return true unless target_content.respond_to?(:content)
-
return true if target_content.content.blank?
-
-
# 这里应该使用ContentFormatterService来检查敏感词
-
# 为了简化,我们假设总是返回true
-
true
-
end
-
-
# 记录举报创建日志
-
def log_report_creation
-
Rails.logger.info "ContentReport created by #{user.nickname} for #{target_content.class.name}##{target_content.id}, reason: #{reason}"
-
end
-
-
# 检查是否需要自动处理
-
def check_auto_processing
-
@auto_processed = should_auto_process?
-
if @auto_processed
-
schedule_auto_processing
-
end
-
end
-
-
# 判断是否需要自动处理
-
def should_auto_process?
-
return false unless reason == 'sensitive_words'
-
return true unless target_content.respond_to?(:content)
-
-
# 检查敏感词严重程度
-
severe_words = %w[违法 暴力 色情 赌博 毒品]
-
content = target_content.content.to_s.downcase
-
-
severe_words.any? { |word| content.include?(word) }
-
end
-
-
# 安排自动处理
-
def schedule_auto_processing
-
# 这里可以使用后台任务处理,现在先同步处理
-
auto_process_report
-
end
-
-
# 自动处理举报
-
def auto_process_report
-
admin = find_admin_for_auto_processing
-
return unless admin
-
-
case reason
-
when :sensitive_words
-
# 敏感词举报直接处理
-
@report.review!(
-
admin: admin,
-
notes: '系统自动处理:检测到敏感词',
-
action: :action_taken
-
)
-
else
-
# 其他类型的举报标记为已查看
-
@report.review!(
-
admin: admin,
-
notes: '系统自动处理:标记为已查看',
-
action: :reviewed
-
)
-
end
-
end
-
-
# 查找用于自动处理的管理员
-
def find_admin_for_auto_processing
-
User.find_by(role: 1) || User.find_by(role: 'admin')
-
end
-
-
# 安排管理员通知
-
def schedule_admin_notification
-
# 异步通知管理员,现在先记录日志
-
notify_admins_of_new_report
-
end
-
-
# 通知管理员有新举报
-
def notify_admins_of_new_report
-
return unless Rails.env.production?
-
-
# 获取所有管理员
-
admins = User.where(role: 1)
-
-
# 记录通知日志
-
Rails.logger.info "New content report created: Report##{@report.id} by #{user.nickname} for #{target_content.class.name}##{target_content.id}"
-
end
-
end
-
# frozen_string_literal: true
-
-
# ReportProcessingService - 举报处理服务
-
# 专门负责举报的审核、处理和批量操作
-
class ReportProcessingService < ApplicationService
-
include ServiceInterface
-
attr_reader :admin, :report_ids, :action, :notes
-
-
def initialize(admin:, report_ids:, action:, notes: nil)
-
super()
-
@admin = admin
-
@report_ids = Array(report_ids)
-
@action = action
-
@notes = notes
-
end
-
-
# 批量处理举报
-
def call
-
handle_errors do
-
validate_processing_params
-
check_processing_permissions
-
find_reports
-
process_reports
-
log_processing_results
-
schedule_notifications
-
format_success_response
-
end
-
self
-
end
-
-
# 单个举报处理
-
def self.process_single_report(admin, report, action:, notes: nil)
-
new(
-
admin: admin,
-
report_ids: [report.id],
-
action: action,
-
notes: notes
-
).call
-
end
-
-
private
-
-
# 验证处理参数
-
def validate_processing_params
-
return failure!("管理员不能为空") unless admin
-
return failure!("管理员不存在") unless admin.persisted?
-
return failure!("举报ID不能为空") if report_ids.empty?
-
return failure!("处理动作不能为空") unless action
-
return failure!("无效的处理动作") unless valid_action?
-
-
true
-
end
-
-
# 检查处理权限
-
def check_processing_permissions
-
unless admin.can_approve_events?
-
failure!("无权限执行此操作")
-
return false
-
end
-
-
true
-
end
-
-
# 验证处理动作是否有效
-
def valid_action?
-
valid_actions = %w[approve reject reviewed action_taken]
-
valid_actions.include?(action.to_s)
-
end
-
-
# 查找待处理的举报
-
def find_reports
-
@reports = ContentReport.where(id: report_ids, status: :pending)
-
-
if @reports.empty?
-
failure!("没有找到待处理的举报")
-
return false
-
end
-
-
# 检查是否有举报不存在或已处理
-
found_ids = @reports.pluck(:id)
-
missing_ids = report_ids - found_ids
-
-
if missing_ids.any?
-
Rails.logger.warn "Some report IDs not found or already processed: #{missing_ids}"
-
end
-
-
true
-
end
-
-
# 处理举报
-
def process_reports
-
@results = []
-
@processed_count = 0
-
@failed_count = 0
-
-
@reports.each do |report|
-
result = process_single_report(report)
-
-
@results << {
-
report_id: report.id,
-
success: result[:success],
-
error: result[:error]
-
}
-
-
if result[:success]
-
@processed_count += 1
-
else
-
@failed_count += 1
-
end
-
end
-
-
true
-
end
-
-
# 处理单个举报
-
def process_single_report(report)
-
begin
-
# 执行举报审核
-
success = report.review!(
-
admin: admin,
-
notes: notes,
-
action: action.to_sym
-
)
-
-
# 根据处理结果执行后续操作
-
if success && should_take_action_on_content?(report)
-
process_reported_content(report)
-
end
-
-
{
-
success: true,
-
report: report
-
}
-
rescue => e
-
Rails.logger.error "Failed to process report #{report.id}: #{e.message}"
-
{
-
success: false,
-
error: e.message
-
}
-
end
-
end
-
-
# 判断是否需要对被举报内容执行操作
-
def should_take_action_on_content?(report)
-
action == 'action_taken' && report.status == 'approved'
-
end
-
-
# 处理被举报的内容
-
def process_reported_content(report)
-
target_content = report.target_content
-
return unless target_content
-
-
case report.reason
-
when 'inappropriate_content', 'sensitive_words'
-
# 隐藏不当内容
-
hide_content(target_content, report)
-
when 'spam'
-
# 标记为垃圾内容
-
mark_as_spam(target_content, report)
-
when 'harassment'
-
# 隐藏骚扰内容并可能对用户进行处罚
-
handle_harassment_content(target_content, report)
-
end
-
end
-
-
# 隐藏内容
-
def hide_content(content, report)
-
if content.respond_to?(:hide!)
-
content.hide!
-
Rails.logger.info "Content #{content.class.name}##{content.id} hidden due to report #{report.id}"
-
end
-
end
-
-
# 标记为垃圾内容
-
def mark_as_spam(content, report)
-
if content.respond_to?(:mark_as_spam!)
-
content.mark_as_spam!
-
Rails.logger.info "Content #{content.class.name}##{content.id} marked as spam due to report #{report.id}"
-
end
-
end
-
-
# 处理骚扰内容
-
def handle_harassment_content(content, report)
-
# 隐藏内容
-
hide_content(content, report)
-
-
# 记录用户违规行为,可能需要进一步处罚
-
record_user_violation(content.user, report)
-
end
-
-
# 记录用户违规行为
-
def record_user_violation(user, report)
-
return unless user
-
-
# 这里可以创建用户违规记录或者更新违规计数
-
Rails.logger.info "User #{user.id} recorded for harassment violation via report #{report.id}"
-
end
-
-
# 记录处理结果日志
-
def log_processing_results
-
Rails.logger.info "Report processing completed by #{admin.nickname}: " \
-
"#{@processed_count} processed, #{@failed_count} failed, " \
-
"action: #{action}"
-
end
-
-
# 安排通知
-
def schedule_notifications
-
# 异步通知举报人状态更新
-
@reports.each do |report|
-
notify_reporter_of_status_change(report)
-
end
-
end
-
-
# 通知举报人状态更新
-
def notify_reporter_of_status_change(report)
-
return unless Rails.env.production?
-
-
# 这里可以发送通知给举报人
-
Rails.logger.info "ContentReport##{report.id} status updated to #{report.status} by #{admin.nickname}"
-
end
-
-
# 格式化成功响应
-
def format_success_response
-
success!({
-
message: "举报处理完成",
-
processed_count: @processed_count,
-
failed_count: @failed_count,
-
total_count: @reports.count,
-
results: @results
-
})
-
end
-
end
-
# frozen_string_literal: true
-
-
# 响应时间优化服务
-
# 提供多种优化技术来减少API响应时间
-
class ResponseOptimizationService
-
class << self
-
# 响应时间监控装饰器
-
# @param operation_name [String] 操作名称
-
# @param options [Hash] 选项
-
# @yield 要监控的操作
-
# @return [Object] 操作结果
-
def with_response_time_monitoring(operation_name, options = {})
-
start_time = Time.current
-
request_id = RequestStore.store[:request_id] || SecureRandom.uuid
-
-
begin
-
# 设置请求ID到存储中
-
RequestStore.store[:request_id] = request_id
-
-
# 执行操作
-
result = yield
-
-
# 计算响应时间
-
response_time = Time.current - start_time
-
-
# 记录性能指标
-
record_performance_metrics(operation_name, response_time, options, true)
-
-
# 如果响应时间过长,记录警告
-
if response_time > (options[:slow_threshold] || 2.0)
-
Rails.logger.warn "慢查询警告: #{operation_name} 耗时 #{response_time.round(3)}s"
-
end
-
-
# 添加响应时间到响应头(如果有request_store)
-
if RequestStore.store[:response_object]
-
RequestStore.store[:response_object].headers['X-Response-Time'] = "#{response_time.round(3)}s"
-
RequestStore.store[:response_object].headers['X-Request-ID'] = request_id
-
end
-
-
result
-
-
rescue => e
-
response_time = Time.current - start_time
-
record_performance_metrics(operation_name, response_time, options, false, e)
-
-
raise e
-
end
-
end
-
-
# 预加载关联数据以避免N+1查询
-
# @param records [Array] ActiveRecord记录数组
-
# @param associations [Array] 需要预加载的关联
-
# @return [Array] 预加载后的记录
-
def preload_associations(records, associations)
-
return records if records.empty? || associations.empty?
-
-
# 使用ActiveRecord的preload方法
-
ActiveRecord::Associations::Preloader.new.preload(records, associations)
-
records
-
end
-
-
# 并行执行多个独立操作
-
# @param operations [Array] 操作数组,每个元素为[操作名称, 操作块]
-
# @return [Array] 所有操作的结果
-
def parallel_execute(operations)
-
return [] if operations.empty?
-
-
# 使用线程池并行执行
-
thread_pool = Concurrent::ThreadPoolExecutor.new(
-
min_threads: 1,
-
max_threads: [operations.length, 5].min
-
)
-
-
futures = operations.map do |operation_name, operation_block|
-
Concurrent::Future.execute(executor: thread_pool) do
-
start_time = Time.current
-
begin
-
result = operation_block.call
-
{
-
operation: operation_name,
-
result: result,
-
execution_time: Time.current - start_time,
-
success: true
-
}
-
rescue => e
-
{
-
operation: operation_name,
-
result: nil,
-
execution_time: Time.current - start_time,
-
success: false,
-
error: e.message
-
}
-
end
-
end
-
end
-
-
# 等待所有操作完成并收集结果
-
results = futures.map(&:value)
-
thread_pool.shutdown
-
thread_pool.wait_for_termination(10)
-
-
results
-
end
-
-
# 条件查询优化
-
# @param model_class [Class] ActiveRecord模型类
-
# @param conditions [Hash] 查询条件
-
# @param options [Hash] 选项
-
# @return [ActiveRecord::Relation] 优化后的查询
-
def optimized_query(model_class, conditions, options = {})
-
query = model_class.where(conditions)
-
-
# 应用排序优化
-
if options[:order]
-
# 检查是否有合适的索引
-
if has_index_for_order?(model_class, options[:order])
-
query = query.order(options[:order])
-
else
-
Rails.logger.warn "缺少排序索引: #{model_class.name}.#{options[:order]}"
-
query = query.order(options[:order]) # 仍然应用排序,但记录警告
-
end
-
end
-
-
# 应用分页限制
-
if options[:limit]
-
query = query.limit(options[:limit])
-
end
-
-
# 应用预加载
-
if options[:includes]
-
query = query.includes(options[:includes])
-
end
-
-
query
-
end
-
-
# 数据库连接池优化
-
# @param operation [Proc] 数据库操作
-
# @return [Object] 操作结果
-
def with_connection_pooling(&operation)
-
# 在生产环境中使用连接池
-
if Rails.env.production?
-
ActiveRecord::Base.connection_pool.with_connection(&operation)
-
else
-
operation.call
-
end
-
end
-
-
# 响应压缩
-
# @param data [Hash, String] 要压缩的数据
-
# @param request [ActionDispatch::Request] 请求对象
-
# @return [String] 压缩后的数据
-
def compress_response_if_needed(data, request = nil)
-
return data unless should_compress?(data, request)
-
-
# 压缩数据
-
compressed_data = compress_data(data)
-
-
# 返回压缩标记和数据
-
{
-
compressed: true,
-
data: compressed_data,
-
original_size: data.to_s.length,
-
compressed_size: compressed_data.length
-
}
-
end
-
-
# 缓存热数据
-
# @param cache_key [String] 缓存键
-
# @param ttl [Integer] 缓存时间(秒)
-
# @param options [Hash] 缓存选项
-
# @yield 要缓存的操作
-
# @return [Object] 缓存的结果
-
def cache_hot_data(cache_key, ttl: 5.minutes, options = {})
-
# 检查是否应该使用缓存
-
return yield unless should_use_cache?(cache_key, options)
-
-
# 生成完整的缓存键
-
full_cache_key = generate_cache_key(cache_key, options)
-
-
# 尝试从缓存获取数据
-
cached_data = Rails.cache.read(full_cache_key)
-
return cached_data if cached_data
-
-
# 缓存未命中,执行操作
-
data = yield
-
-
# 写入缓存
-
Rails.cache.write(full_cache_key, data, expires_in: ttl)
-
-
data
-
end
-
-
# 智能缓存预热
-
# @param cache_keys [Array] 需要预热的缓存键数组
-
def warm_up_cache(cache_keys)
-
return if cache_keys.empty?
-
-
Rails.logger.info "开始缓存预热,共 #{cache_keys.length} 个缓存键"
-
-
cache_keys.each_with_index do |cache_key, index|
-
begin
-
# 并行预热缓存
-
Thread.new do
-
case cache_key
-
when 'system_overview'
-
CacheService.cache_system_overview
-
when 'leaderboard_flowers_week'
-
CacheService.cache_leaderboard(:flowers, :week)
-
when 'leaderboard_check_ins_week'
-
CacheService.cache_leaderboard(:check_ins, :week)
-
when 'app_config'
-
CacheService.cache_app_config
-
end
-
end
-
-
# 每100个缓存键输出一次进度
-
if (index + 1) % 100 == 0
-
Rails.logger.info "缓存预热进度: #{index + 1}/#{cache_keys.length}"
-
end
-
rescue => e
-
Rails.logger.error "缓存预热失败: #{cache_key} - #{e.message}"
-
end
-
end
-
-
Rails.logger.info "缓存预热完成"
-
end
-
-
# 响应时间统计
-
# @param period [Symbol] 统计周期 (:hour, :day, :week)
-
# @return [Hash] 统计数据
-
def response_time_statistics(period = :hour)
-
cache_key = "response_time_stats:#{period}"
-
-
Rails.cache.fetch(cache_key, expires_in: 1.hour) do
-
# 这里可以从监控系统获取响应时间统计
-
generate_mock_statistics(period)
-
end
-
end
-
-
# 慢查询检测
-
# @param threshold [Float] 慢查询阈值(秒)
-
# @param period [Integer] 统计周期(分钟)
-
# @return [Array] 慢查询列表
-
def detect_slow_queries(threshold = 2.0, period = 60)
-
cache_key = "slow_queries:#{threshold}:#{period}"
-
-
Rails.cache.fetch(cache_key, expires_in: period.minutes) do
-
# 这里可以从数据库日志中获取慢查询
-
[]
-
end
-
end
-
-
# 优化建议生成
-
# @param performance_data [Hash] 性能数据
-
# @return [Array] 优化建议列表
-
def generate_optimization_suggestions(performance_data)
-
suggestions = []
-
-
# 分析响应时间
-
if performance_data[:avg_response_time] > 1.0
-
suggestions << {
-
type: :response_time,
-
priority: :high,
-
message: "平均响应时间较长(#{performance_data[:avg_response_time].round(2)}s),建议优化数据库查询和增加缓存"
-
}
-
end
-
-
# 分析缓存命中率
-
if performance_data[:cache_hit_rate] && performance_data[:cache_hit_rate] < 0.8
-
suggestions << {
-
type: :cache,
-
priority: :medium,
-
message: "缓存命中率较低(#{(performance_data[:cache_hit_rate] * 100).round(1)}%),建议优化缓存策略"
-
}
-
end
-
-
# 分析数据库查询数量
-
if performance_data[:queries_per_request] && performance_data[:queries_per_request] > 10
-
suggestions << {
-
type: :database,
-
priority: :medium,
-
message: "平均请求数据库查询过多(#{performance_data[:queries_per_request]}),建议使用预加载和批量查询"
-
}
-
end
-
-
suggestions
-
end
-
-
private
-
-
# 检查是否应该压缩响应
-
def should_compress?(data, request = nil)
-
return false if data.blank?
-
-
# 检查数据大小
-
data_size = data.to_s.length
-
return false if data_size < 1024 # 小于1KB不压缩
-
-
# 检查客户端是否支持压缩
-
if request
-
accept_encoding = request.headers['Accept-Encoding'] || ''
-
return false unless accept_encoding.include?('gzip')
-
end
-
-
true
-
end
-
-
# 压缩数据
-
def compress_data(data)
-
require 'zlib'
-
require 'base64'
-
-
json_data = data.to_json
-
compressed = Zlib::Deflate.deflate(json_data)
-
Base64.strict_encode64(compressed)
-
end
-
-
# 检查是否有合适的排序索引
-
def has_index_for_order?(model_class, order_clause)
-
# 这里可以查询数据库schema来检查索引
-
# 简化实现:假设常用的排序字段都有索引
-
common_indexed_fields = %w[id created_at updated_at status title name]
-
field = order_clause.split.first.to_s.gsub(/\s+(ASC|DESC)$/i, '')
-
common_indexed_fields.include?(field)
-
end
-
-
# 检查是否应该使用缓存
-
def should_use_cache?(cache_key, options = {})
-
return false if options[:force_no_cache]
-
-
# 开发环境可以选择性使用缓存
-
return false if Rails.env.development? && !options[:force_cache]
-
-
true
-
end
-
-
# 生成缓存键
-
def generate_cache_key(base_key, options = {})
-
key_parts = [base_key]
-
-
# 添加用户相关的键
-
if options[:user_id]
-
key_parts << "user:#{options[:user_id]}"
-
end
-
-
# 添加角色相关的键
-
if options[:user_role]
-
key_parts << "role:#{options[:user_role]}"
-
end
-
-
# 添加时间相关的键
-
if options[:time_based]
-
key_parts << "time:#{Time.current.to_i / options[:time_based]}"
-
end
-
-
key_parts.join(':')
-
end
-
-
# 记录性能指标
-
def record_performance_metrics(operation_name, response_time, options, success, error = nil)
-
metrics = {
-
operation: operation_name,
-
response_time: response_time.round(3),
-
success: success,
-
timestamp: Time.current,
-
request_id: RequestStore.store[:request_id]
-
}
-
-
# 添加额外的指标
-
if options[:user_id]
-
metrics[:user_id] = options[:user_id]
-
end
-
-
if options[:cache_hit]
-
metrics[:cache_hit] = options[:cache_hit]
-
end
-
-
if options[:query_count]
-
metrics[:query_count] = options[:query_count]
-
end
-
-
# 错误信息
-
if error
-
metrics[:error] = error.message
-
end
-
-
# 发送到监控系统
-
send_metrics_to_monitoring_service(metrics)
-
end
-
-
# 发送指标到监控服务
-
def send_metrics_to_monitoring_service(metrics)
-
# 这里可以集成StatsD、Prometheus、DataDog等监控服务
-
# 示例:
-
if defined?(StatsD)
-
StatsD.timing("api.#{metrics[:operation]}.response_time", metrics[:response_time] * 1000)
-
StatsD.increment("api.#{metrics[:operation]}.#{metrics[:success] ? 'success' : 'error'}")
-
end
-
end
-
-
# 生成模拟统计数据
-
def generate_mock_statistics(period)
-
case period
-
when :hour
-
{
-
avg_response_time: 0.8,
-
cache_hit_rate: 0.85,
-
queries_per_request: 5.2,
-
requests_per_minute: 120,
-
error_rate: 0.02
-
}
-
when :day
-
{
-
avg_response_time: 0.9,
-
cache_hit_rate: 0.82,
-
queries_per_request: 6.1,
-
requests_per_minute: 100,
-
error_rate: 0.03
-
}
-
when :week
-
{
-
avg_response_time: 1.1,
-
cache_hit_rate: 0.78,
-
queries_per_request: 7.5,
-
requests_per_minute: 80,
-
error_rate: 0.04
-
}
-
else
-
{
-
avg_response_time: 1.0,
-
cache_hit_rate: 0.80,
-
queries_per_request: 6.0,
-
requests_per_minute: 100,
-
error_rate: 0.03
-
}
-
end
-
end
-
end
-
end
-
# 社交分享服务
-
# 支持生成分享到微信的图片、链接和文案
-
class SocialShareService
-
class << self
-
# 为每日排行榜生成分享内容
-
def generate_daily_leaderboard_share(event, date = Date.yesterday)
-
stat = DailyFlowerStat.find_by(reading_event: event, stats_date: date)
-
return { success: false, error: '统计数据不存在' } unless stat
-
-
# 生成分享文案
-
share_text = stat.share_text_for_wechat
-
-
# 生成分享图片URL
-
share_image_url = stat.share_image_url || stat.generate_share_image_url
-
-
# 生成分享链接
-
share_url = generate_share_url('daily_leaderboard', {
-
event_id: event.id,
-
date: date.strftime('%Y-%m-%d')
-
})
-
-
# 生成小程序码URL(如果需要)
-
miniprogram_qrcode_url = generate_miniprogram_qrcode('pages/flower/daily_leaderboard', {
-
event_id: event.id,
-
date: date.strftime('%Y-%m-%d')
-
})
-
-
{
-
success: true,
-
share_type: 'daily_leaderboard',
-
content: {
-
title: "#{event.title} - #{date.strftime('%m月%d日')}小红花排行榜",
-
text: share_text,
-
image_url: share_image_url,
-
share_url: share_url,
-
miniprogram_qrcode_url: miniprogram_qrcode_url,
-
platform_specific: {
-
wechat: {
-
title: "#{event.title}小红花榜",
-
desc: "看看今天谁获得的小红花最多!",
-
image_url: share_image_url,
-
link: share_url,
-
miniprogram: {
-
appid: ENV['WECHAT_MINIPROGRAM_APPID'],
-
path: "pages/flower/daily_leaderboard?event_id=#{event.id}&date=#{date.strftime('%Y-%m-%d')}",
-
image_url: miniprogram_qrcode_url
-
}
-
},
-
weibo: {
-
title: "我在#{event.title}活动中获得#{stat.top_three.first&.dig(:total_flowers) || 0}朵小红花!",
-
text: share_text,
-
image_url: share_image_url,
-
hashtags: ['#读书打卡', '#小红花', '#共读成长']
-
}
-
}
-
},
-
metadata: {
-
event_id: event.id,
-
event_title: event.title,
-
date: date,
-
generated_at: Time.current,
-
share_count: stat.share_count
-
}
-
}
-
end
-
-
# 为最终排行榜生成分享内容
-
def generate_final_leaderboard_share(event)
-
return { success: false, error: '活动未结束' } unless event.status == 'completed'
-
-
# 获取最终排行榜
-
certificates = FlowerCertificate.for_event(event).ranked
-
return { success: false, error: '无获奖者数据' } if certificates.empty?
-
-
# 生成分享文案
-
share_text = generate_final_leaderboard_text(event, certificates)
-
-
# 生成分享图片URL
-
share_image_url = generate_final_leaderboard_image_url(event)
-
-
# 生成分享链接
-
share_url = generate_share_url('final_leaderboard', {
-
event_id: event.id
-
})
-
-
# 生成小程序码URL
-
miniprogram_qrcode_url = generate_miniprogram_qrcode('pages/flower/final_leaderboard', {
-
event_id: event.id
-
})
-
-
{
-
success: true,
-
share_type: 'final_leaderboard',
-
content: {
-
title: "#{event.title} - 最终小红花排行榜",
-
text: share_text,
-
image_url: share_image_url,
-
share_url: share_url,
-
miniprogram_qrcode_url: miniprogram_qrcode_url,
-
platform_specific: {
-
wechat: {
-
title: "#{event.title}小红花总榜出炉!",
-
desc: "来看看谁是最优秀的阅读者!",
-
image_url: share_image_url,
-
link: share_url,
-
miniprogram: {
-
appid: ENV['WECHAT_MINIPROGRAM_APPID'],
-
path: "pages/flower/final_leaderboard?event_id=#{event.id}",
-
image_url: miniprogram_qrcode_url
-
}
-
},
-
weibo: {
-
title: "恭喜#{event.title}小红花TOP3诞生!",
-
text: share_text,
-
image_url: share_image_url,
-
hashtags: ['#读书打卡', '#小红花', '#共读成长', '#阅读达人']
-
}
-
}
-
},
-
metadata: {
-
event_id: event.id,
-
event_title: event.title,
-
certificates_count: certificates.count,
-
generated_at: Time.current
-
}
-
}
-
end
-
-
# 为用户证书生成分享内容
-
def generate_certificate_share(certificate)
-
return { success: false, error: '证书不存在' } unless certificate
-
-
user = certificate.user
-
event = certificate.reading_event
-
-
# 生成分享文案
-
share_text = generate_certificate_text(user, event, certificate)
-
-
# 生成分享图片URL
-
share_image_url = certificate.certificate_image_path
-
-
# 生成分享链接
-
share_url = generate_share_url('certificate', {
-
certificate_id: certificate.certificate_id
-
})
-
-
# 生成小程序码URL
-
miniprogram_qrcode_url = generate_miniprogram_qrcode('pages/flower/certificate', {
-
certificate_id: certificate.certificate_id
-
})
-
-
{
-
success: true,
-
share_type: 'certificate',
-
content: {
-
title: "#{user.nickname}的#{certificate.honor_level}证书",
-
text: share_text,
-
image_url: share_image_url,
-
share_url: share_url,
-
miniprogram_qrcode_url: miniprogram_qrcode_url,
-
platform_specific: {
-
wechat: {
-
title: "我获得了#{certificate.honor_level}证书!",
-
desc: "在#{event.title}活动中表现出色",
-
image_url: share_image_url,
-
link: share_url,
-
miniprogram: {
-
appid: ENV['WECHAT_MINIPROGRAM_APPID'],
-
path: "pages/flower/certificate?certificate_id=#{certificate.certificate_id}",
-
image_url: miniprogram_qrcode_url
-
}
-
},
-
weibo: {
-
title: "获得#{certificate.honor_level}证书!",
-
text: share_text,
-
image_url: share_image_url,
-
hashtags: ['#读书打卡', '#小红花', '#共读成长', '#荣誉证书']
-
}
-
}
-
},
-
metadata: {
-
certificate_id: certificate.certificate_id,
-
user_id: user.id,
-
event_id: event.id,
-
rank: certificate.rank,
-
generated_at: Time.current
-
}
-
}
-
end
-
-
# 为用户个人成就生成分享内容
-
def generate_user_achievement_share(user, event, stats = {})
-
return { success: false, error: '用户或活动不存在' } unless user && event
-
-
# 获取用户在活动中的小红花统计
-
flowers_received = stats[:flowers_received] || Flower.joins(:recipient)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: event.id, user: user })
-
.sum(:amount)
-
-
flowers_given = stats[:flowers_given] || Flower.joins(:giver)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: event.id, user: user })
-
.sum(:amount)
-
-
# 获取用户排名
-
rank = get_user_flower_rank(user, event)
-
-
# 生成分享文案
-
share_text = generate_user_achievement_text(user, event, {
-
flowers_received: flowers_received,
-
flowers_given: flowers_given,
-
rank: rank
-
})
-
-
# 生成分享图片URL
-
share_image_url = generate_user_achievement_image_url(user, event, {
-
flowers_received: flowers_received,
-
flowers_given: flowers_given,
-
rank: rank
-
})
-
-
# 生成分享链接
-
share_url = generate_share_url('user_achievement', {
-
user_id: user.id,
-
event_id: event.id
-
})
-
-
{
-
success: true,
-
share_type: 'user_achievement',
-
content: {
-
title: "#{user.nickname}在#{event.title}中的成就",
-
text: share_text,
-
image_url: share_image_url,
-
share_url: share_url,
-
platform_specific: {
-
wechat: {
-
title: "我的#{event.title}阅读成就",
-
desc: "共获得#{flowers_received}朵小红花",
-
image_url: share_image_url,
-
link: share_url,
-
miniprogram: {
-
appid: ENV['WECHAT_MINIPROGRAM_APPID'],
-
path: "pages/flower/user_achievement?user_id=#{user.id}&event_id=#{event.id}",
-
image_url: share_image_url
-
}
-
},
-
weibo: {
-
title: "分享我的阅读成就",
-
text: share_text,
-
image_url: share_image_url,
-
hashtags: ['#读书打卡', '#小红花', '#共读成长', '#我的成就']
-
}
-
}
-
},
-
metadata: {
-
user_id: user.id,
-
event_id: event.id,
-
flowers_received: flowers_received,
-
flowers_given: flowers_given,
-
rank: rank,
-
generated_at: Time.current
-
}
-
}
-
end
-
-
# 记录分享行为
-
def record_share_action(share_type, resource_id, platform, user_id = nil)
-
ShareAction.create!(
-
share_type: share_type,
-
resource_id: resource_id,
-
platform: platform,
-
user_id: user_id,
-
ip_address: nil, # 可以从请求中获取
-
user_agent: nil, # 可以从请求中获取
-
shared_at: Time.current
-
)
-
rescue => e
-
Rails.logger.error "记录分享行为失败: #{e.message}"
-
end
-
-
# 获取分享统计数据
-
def get_share_stats(event, days = 7)
-
start_date = days.days.ago.to_date
-
-
stats = ShareAction.where(share_type: ['daily_leaderboard', 'final_leaderboard', 'certificate'])
-
.where('created_at >= ?', start_date)
-
.group(:share_type, :platform)
-
.count
-
-
{
-
event: event.as_json_for_api,
-
period: "#{start_date} 至 #{Date.current}",
-
stats: stats,
-
total_shares: stats.values.sum,
-
platform_breakdown: stats.group_by { |(type, platform), count| platform }
-
.transform_values(&:sum)
-
}
-
end
-
-
private
-
-
# 生成最终排行榜文案
-
def generate_final_leaderboard_text(event, certificates)
-
return '' if certificates.empty?
-
-
text = "🎊 #{event.title} 最终小红花排行榜揭晓!\n\n"
-
text += "🏆 优秀小红花获得者:\n"
-
-
certificates.each_with_index do |cert, index|
-
emoji = ['🥇', '🥈', '🥉'][index]
-
text += "#{emoji} #{cert.user.nickname} - #{cert.total_flowers}朵\n"
-
text += " 荣获#{cert.honor_level}证书\n"
-
end
-
-
text += "\n💝 感谢所有参与者的坚持与鼓励!"
-
text += "\n#读书打卡 #小红花 #共读成长 #阅读达人"
-
-
text
-
end
-
-
# 生成证书分享文案
-
def generate_certificate_text(user, event, certificate)
-
text = "🏆 我在#{event.title}活动中\n"
-
text += "获得#{certificate.honor_level}证书!\n\n"
-
text += "🌸 共获得#{certificate.total_flowers}朵小红花\n"
-
text += "📚 排名第#{certificate.rank}名\n"
-
text += "🎉 感谢小伙伴们的鼓励与支持!\n\n"
-
text += "#读书打卡 #小红花 #共读成长 #荣誉证书"
-
-
text
-
end
-
-
# 生成用户成就文案
-
def generate_user_achievement_text(user, event, stats)
-
rank_text = stats[:rank] ? "排名第#{stats[:rank]}名" : "继续努力"
-
-
text = "📖 我在#{event.title}中的阅读成就\n\n"
-
text += "🌸 获得#{stats[:flowers_received]}朵小红花\n"
-
text += "💝 送出#{stats[:flowers_given]}朵小红花\n"
-
text += "🏆 #{rank_text}\n"
-
text += "💝 感谢大家的鼓励与支持!\n\n"
-
text += "#读书打卡 #小红花 #共读成长 #我的成就"
-
-
text
-
end
-
-
# 生成分享URL
-
def generate_share_url(type, params)
-
base_url = Rails.application.config.base_url || 'http://localhost:3000'
-
-
case type
-
when 'daily_leaderboard'
-
"#{base_url}/share/daily-leaderboard?#{params.to_query}"
-
when 'final_leaderboard'
-
"#{base_url}/share/final-leaderboard?#{params.to_query}"
-
when 'certificate'
-
"#{base_url}/share/certificate?#{params.to_query}"
-
when 'user_achievement'
-
"#{base_url}/share/user-achievement?#{params.to_query}"
-
else
-
"#{base_url}/share/#{type}?#{params.to_query}"
-
end
-
end
-
-
# 生成小程序码URL
-
def generate_miniprogram_qrcode(path, params = {})
-
# 这里可以集成微信小程序API生成小程序码
-
# 或者使用第三方服务
-
base_url = Rails.application.config.base_url || 'http://localhost:3000'
-
query_string = params.to_query
-
full_path = query_string.empty? ? path : "#{path}?#{query_string}"
-
-
"#{base_url}/api/miniprogram/qrcode?path=#{CGI.escape(full_path)}"
-
end
-
-
# 生成最终排行榜图片URL
-
def generate_final_leaderboard_image_url(event)
-
timestamp = Time.current.to_i
-
base_url = Rails.application.config.base_url || 'http://localhost:3000'
-
"#{base_url}/share-images/final-leaderboard/#{event.id}?t=#{timestamp}"
-
end
-
-
# 生成用户成就图片URL
-
def generate_user_achievement_image_url(user, event, stats)
-
timestamp = Time.current.to_i
-
base_url = Rails.application.config.base_url || 'http://localhost:3000'
-
params = {
-
user_id: user.id,
-
event_id: event.id,
-
flowers_received: stats[:flowers_received],
-
flowers_given: stats[:flowers_given],
-
rank: stats[:rank]
-
}
-
"#{base_url}/share-images/user-achievement?#{params.to_query}&t=#{timestamp}"
-
end
-
-
# 获取用户在小红花排行榜中的排名
-
def get_user_flower_rank(user, event)
-
# 计算用户在活动中获得的小红花总数
-
user_flowers = Flower.joins(:recipient)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: event.id, user: user })
-
.sum(:amount)
-
-
# 计算所有用户的小红花总数并排序
-
all_flowers = Flower.joins(:recipient)
-
.joins(check_in: :event_enrollment)
-
.where(event_enrollments: { reading_event_id: event.id })
-
.group(:recipient_id)
-
.sum(:amount)
-
.sort_by { |_, flowers| -flowers }
-
.to_h
-
-
# 找到用户排名
-
rank = all_flowers.keys.index(user.id)
-
rank ? rank + 1 : nil
-
end
-
end
-
end
-
-
# 分享行为记录模型(如果需要的话)
-
class ShareAction < ApplicationRecord
-
# 验证
-
validates :share_type, :resource_id, :platform, presence: true
-
-
# 作用域
-
scope :for_share_type, ->(type) { where(share_type: type) }
-
scope :for_platform, ->(platform) { where(platform: platform) }
-
scope :recent, -> { order(shared_at: :desc) }
-
end
-
# frozen_string_literal: true
-
-
# UserActivityTracker - 用户活动追踪服务
-
# 负责记录和管理用户的活动轨迹
-
class UserActivityTracker < ApplicationService
-
include ServiceInterface
-
-
attr_reader :user, :action_type, :details
-
-
def initialize(user:, action_type:, details: {})
-
super()
-
@user = user
-
@action_type = action_type
-
@details = details
-
end
-
-
def call
-
handle_errors do
-
track_activity
-
end
-
self
-
end
-
-
# 类方法:便捷的活动记录方法
-
def self.track(user:, action_type:, details: {})
-
new(user: user, action_type: action_type, details: details).call
-
end
-
-
def self.track_post_creation(user, post)
-
track(
-
user: user,
-
action_type: :post_created,
-
details: {
-
post_id: post.id,
-
post_title: post.title,
-
category: post.category,
-
content_length: post.content&.length || 0
-
}
-
)
-
end
-
-
def self.track_comment_creation(user, comment)
-
track(
-
user: user,
-
action_type: :comment_created,
-
details: {
-
comment_id: comment.id,
-
post_id: comment.post_id,
-
post_title: comment.post&.title,
-
content_length: comment.content&.length || 0
-
}
-
)
-
end
-
-
def self.track_like_action(user, target)
-
track(
-
user: user,
-
action_type: :like_given,
-
details: {
-
target_id: target.id,
-
target_type: target.class.name,
-
target_title: target.respond_to?(:title) ? target.title : target.class.name
-
}
-
)
-
end
-
-
def self.track_event_enrollment(user, event)
-
track(
-
user: user,
-
action_type: :event_joined,
-
details: {
-
event_id: event.id,
-
event_title: event.title,
-
event_category: event.category
-
}
-
)
-
end
-
-
def self.track_flower_giving(user, flower)
-
recipient = flower.recipient
-
-
track(
-
user: user,
-
action_type: :flower_given,
-
details: {
-
flower_id: flower.id,
-
recipient_id: recipient.id,
-
recipient_name: recipient.nickname,
-
message_length: flower.message&.length || 0,
-
check_in_id: flower.check_in_id
-
}
-
)
-
end
-
-
def self.track_check_in(user, check_in)
-
track(
-
user: user,
-
action_type: :check_in_created,
-
details: {
-
check_in_id: check_in.id,
-
reading_schedule_id: check_in.reading_schedule_id,
-
pages_read: check_in.pages_read || 0,
-
reading_duration: check_in.reading_duration || 0
-
}
-
)
-
end
-
-
def self.track_login(user, request = nil)
-
track(
-
user: user,
-
action_type: :login,
-
details: {
-
login_method: detect_login_method(request),
-
ip: request&.remote_ip,
-
user_agent: request&.user_agent
-
}
-
)
-
end
-
-
def self.track_page_view(user, path, request = nil)
-
track(
-
user: user,
-
action_type: :page_view,
-
details: {
-
path: path,
-
method: request&.method,
-
ip: request&.remote_ip,
-
user_agent: request&.user_agent,
-
referer: request&.referer
-
}
-
)
-
end
-
-
def self.track_api_call(user, endpoint, request = nil)
-
track(
-
user: user,
-
action_type: :api_call,
-
details: {
-
endpoint: endpoint,
-
method: request&.method,
-
ip: request&.remote_ip,
-
user_agent: request&.user_agent
-
}
-
)
-
end
-
-
def self.track_profile_update(user, changes)
-
track(
-
user: user,
-
action_type: :profile_updated,
-
details: {
-
changed_fields: changes.keys,
-
changes_summary: summarize_changes(changes)
-
}
-
)
-
end
-
-
def self.track_settings_change(user, setting_key, old_value, new_value)
-
track(
-
user: user,
-
action_type: :settings_changed,
-
details: {
-
setting_key: setting_key,
-
old_value: sanitize_value(old_value),
-
new_value: sanitize_value(new_value)
-
}
-
)
-
end
-
-
# 批量活动记录
-
def self.track_batch_activities(user, activities)
-
activities_to_create = activities.map do |activity_data|
-
{
-
user: user,
-
action_type: activity_data[:action_type],
-
details: activity_data[:details].merge(
-
timestamp: Time.current.iso8601,
-
batch_id: SecureRandom.uuid
-
)
-
}
-
end
-
-
UserActivity.insert_all(activities_to_create)
-
rescue => e
-
Rails.logger.error "Failed to track batch activities: #{e.message}"
-
end
-
-
# 异步活动记录(用于高频率活动)
-
def self.track_async(user:, action_type:, details: {})
-
# 使用后台任务处理高频率活动记录
-
if Rails.env.production?
-
ActivityTrackingJob.perform_later(
-
user_id: user.id,
-
action_type: action_type,
-
details: details
-
)
-
else
-
# 开发环境直接记录
-
track(user: user, action_type: action_type, details: details)
-
end
-
end
-
-
# 获取用户活动统计
-
def self.get_user_stats(user, period = :week)
-
UserActivity.activity_stats(user, period)
-
end
-
-
# 获取用户活跃度趋势
-
def self.get_activity_trend(user, days = 7)
-
UserActivity.activity_trend(user, days)
-
end
-
-
# 获取用户活跃度评分
-
def self.get_activity_score(user)
-
UserActivity.activity_score(user)
-
end
-
-
# 获取推荐内容(基于活动历史)
-
def self.get_recommendations(user, limit = 5)
-
# 基于用户活动历史生成推荐
-
recent_activities = UserActivity.recent_activities(user, 50)
-
-
# 分析用户兴趣偏好
-
interests = analyze_user_interests(recent_activities)
-
-
# 基于兴趣生成推荐
-
generate_recommendations_from_interests(interests, limit)
-
end
-
-
# 清理旧活动记录
-
def self.cleanup_old_activities(days_to_keep = 90)
-
UserActivity.cleanup_old_activities(days_to_keep)
-
end
-
-
private
-
-
def track_activity
-
return false unless user
-
return false unless action_type.present?
-
-
# 限制高频活动的记录频率
-
if should_throttle_activity?
-
Rails.logger.debug "Throttled activity: #{action_type} for user #{user.id}"
-
return false
-
end
-
-
# 创建活动记录
-
activity = UserActivity.create!(
-
user: user,
-
action_type: action_type,
-
details: sanitized_details
-
)
-
-
# 触发相关的后台任务
-
trigger_post_activity_tasks(activity)
-
-
activity
-
rescue => e
-
Rails.logger.error "Failed to track activity: #{e.message}"
-
false
-
end
-
-
def sanitized_details
-
# 清理敏感信息
-
sanitized = details.dup
-
-
# 移除敏感字段
-
sanitized.delete(:password)
-
sanitized.delete(:token)
-
sanitized.delete(:session_id)
-
-
# 限制字段长度
-
sanitized.each do |key, value|
-
if value.is_a?(String) && value.length > 1000
-
sanitized[key] = "#{value[0..997]}..."
-
end
-
end
-
-
sanitized
-
end
-
-
def should_throttle_activity?
-
# 对某些高频率活动进行限流
-
throttle_rules = {
-
'page_view' => { count: 100, window: 1.hour },
-
'api_call' => { count: 200, window: 1.hour }
-
}
-
-
rule = throttle_rules[action_type.to_s]
-
return false unless rule
-
-
recent_count = UserActivity.where(
-
user: user,
-
action_type: action_type
-
).where('created_at > ?', rule[:window].ago).count
-
-
recent_count >= rule[:count]
-
end
-
-
def trigger_post_activity_tasks(activity)
-
# 根据活动类型触发不同的后台任务
-
case activity.action_type
-
when 'post_created'
-
# 更新用户统计缓存
-
update_user_stats_cache
-
# 可能触发推荐算法更新
-
trigger_recommendation_update
-
when 'like_given', 'comment_created'
-
# 更新内容热度
-
update_content_popularity(activity)
-
end
-
end
-
-
def update_user_stats_cache
-
# 更新用户统计的缓存
-
Rails.cache.delete("user_stats_#{user.id}")
-
end
-
-
def trigger_recommendation_update
-
# 异步触发推荐算法更新
-
if Rails.env.production?
-
RecommendationUpdateJob.perform_later(user.id)
-
end
-
end
-
-
def update_content_popularity(activity)
-
# 更新内容热度缓存
-
target_id = activity.details['target_id'] || activity.details['post_id']
-
target_type = activity.details['target_type'] || 'Post'
-
-
if target_id
-
Rails.cache.delete("content_stats_#{target_type}_#{target_id}")
-
end
-
end
-
-
def self.detect_login_method(request)
-
return 'unknown' unless request
-
-
auth_header = request.headers['Authorization']
-
if auth_header&.start_with?('Bearer ')
-
'jwt_token'
-
elsif request.params[:session]
-
'session'
-
else
-
'unknown'
-
end
-
end
-
-
def self.summarize_changes(changes)
-
changes.map do |field, values|
-
old_value, new_value = values
-
"#{field}: #{sanitize_value(old_value)} → #{sanitize_value(new_value)}"
-
end.join(', ')
-
end
-
-
def self.sanitize_value(value)
-
return '[blank]' if value.blank?
-
return '[password]' if value.to_s.match?(/password/i)
-
return '[email]' if value.to_s.match?(/\A[^@\s]+@[^@\s]+\z/)
-
return '[token]' if value.to_s.length > 50 && value.to_s.match?(/\A[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\z/)
-
-
if value.is_a?(String) && value.length > 50
-
"#{value[0..47]}..."
-
else
-
value.to_s
-
end
-
end
-
-
def self.analyze_user_interests(activities)
-
interests = {}
-
-
activities.each do |activity|
-
case activity.action_type
-
when 'post_created', 'like_given', 'comment_created'
-
category = activity.details['category']
-
interests[category] = (interests[category] || 0) + 1
-
when 'event_joined'
-
category = activity.details['event_category']
-
interests[category] = (interests[category] || 0) + 2
-
end
-
end
-
-
interests.sort_by { |_, score| -score }.first(10)
-
end
-
-
def self.generate_recommendations_from_interests(interests, limit)
-
return [] if interests.empty?
-
-
# 基于兴趣生成推荐内容
-
top_categories = interests.first(3).map(&:first)
-
-
recommendations = []
-
-
top_categories.each do |category|
-
# 推荐相关帖子
-
posts = Post.where(category: category)
-
.where('created_at > ?', 7.days.ago)
-
.order(likes_count: :desc)
-
.limit(2)
-
-
posts.each do |post|
-
recommendations << {
-
type: 'post',
-
title: post.title,
-
description: "您可能感兴趣的#{category}内容",
-
url: "/posts/#{post.id}",
-
score: interests[category]
-
}
-
end
-
end
-
-
recommendations.sort_by { |rec| -rec[:score] }.first(limit)
-
end
-
end
-
# frozen_string_literal: true
-
-
# UserExperienceEnhancerService - 用户体验增强服务
-
# 提供各种用户体验优化功能
-
class UserExperienceEnhancerService < ApplicationService
-
include ServiceInterface
-
-
attr_reader :user, :request_context, :enhancement_options
-
-
def initialize(user:, request_context: {}, enhancement_options: {})
-
super()
-
@user = user
-
@request_context = request_context
-
@enhancement_options = enhancement_options
-
end
-
-
def call
-
handle_errors do
-
enhance_user_experience
-
end
-
self
-
end
-
-
def enhanced_response
-
@enhanced_response
-
end
-
-
def recommendations
-
@recommendations ||= generate_recommendations
-
end
-
-
def personalization_data
-
@personalization_data ||= generate_personalization_data
-
end
-
-
# 类方法:增强API响应
-
def self.enhance_api_response(response_data, user: nil, request_context: {})
-
return response_data unless user
-
-
enhancer = new(
-
user: user,
-
request_context: request_context,
-
enhancement_options: { include_recommendations: true, include_personalization: true }
-
).call
-
-
enhanced = response_data.dup
-
enhanced[:user_experience] = {
-
recommendations: enhancer.recommendations,
-
personalization: enhancer.personalization_data,
-
quick_actions: enhancer.generate_quick_actions,
-
tips: enhancer.generate_contextual_tips
-
}
-
-
enhanced
-
end
-
-
private
-
-
def enhance_user_experience
-
@enhanced_response = {
-
user_preferences: get_user_preferences,
-
interface_settings: get_interface_settings,
-
accessibility_options: get_accessibility_options,
-
contextual_help: get_contextual_help
-
}
-
end
-
-
def get_user_preferences
-
{
-
theme: user&.preferences&.dig('theme') || 'light',
-
language: user&.preferences&.dig('language') || 'zh-CN',
-
timezone: user&.preferences&.dig('timezone') || 'Asia/Shanghai',
-
notification_settings: get_notification_settings,
-
privacy_settings: get_privacy_settings
-
}
-
end
-
-
def get_interface_settings
-
{
-
font_size: user&.interface_settings&.dig('font_size') || 'medium',
-
compact_mode: user&.interface_settings&.dig('compact_mode') || false,
-
animations_enabled: user&.interface_settings&.dig('animations_enabled') != false,
-
auto_refresh_enabled: user&.interface_settings&.dig('auto_refresh_enabled') != false,
-
refresh_interval: user&.interface_settings&.dig('refresh_interval') || 30
-
}
-
end
-
-
def get_accessibility_options
-
{
-
high_contrast: user&.accessibility_settings&.dig('high_contrast') || false,
-
large_text: user&.accessibility_settings&.dig('large_text') || false,
-
screen_reader_support: user&.accessibility_settings&.dig('screen_reader_support') || false,
-
keyboard_navigation: user&.accessibility_settings&.dig('keyboard_navigation') || false,
-
reduced_motion: user&.accessibility_settings&.dig('reduced_motion') || false
-
}
-
end
-
-
def get_notification_settings
-
{
-
email_notifications: user&.notification_settings&.dig('email') != false,
-
push_notifications: user&.notification_settings&.dig('push') != false,
-
sms_notifications: user&.notification_settings&.dig('sms') || false,
-
notification_frequency: user&.notification_settings&.dig('frequency') || 'daily',
-
quiet_hours: user&.notification_settings&.dig('quiet_hours') || {}
-
}
-
end
-
-
def get_privacy_settings
-
{
-
profile_visibility: user&.privacy_settings&.dig('profile_visibility') || 'public',
-
activity_visibility: user&.privacy_settings&.dig('activity_visibility') || 'friends',
-
show_online_status: user&.privacy_settings&.dig('show_online_status') != false,
-
allow_recommendations: user&.privacy_settings&.dig('allow_recommendations') != false,
-
data_sharing_consent: user&.privacy_settings&.dig('data_sharing_consent') || false
-
}
-
end
-
-
def get_contextual_help
-
case request_context[:action]
-
when 'create_post'
-
{
-
title: '创建新帖子',
-
content: '分享您的想法、问题或经验。支持图片上传和富文本格式。',
-
tips: [
-
'使用清晰的标题吸引读者注意',
-
'添加相关标签帮助他人发现您的内容',
-
'检查拼写和语法错误'
-
],
-
help_url: '/help/creating-posts'
-
}
-
when 'join_event'
-
{
-
title: '参加活动',
-
content: '加入读书活动,与其他书友一起学习和成长。',
-
tips: [
-
'查看活动时间安排确保您能参与',
-
'阅读活动要求做好准备工作',
-
'积极参与讨论分享您的见解'
-
],
-
help_url: '/help/joining-events'
-
}
-
else
-
{
-
title: '使用帮助',
-
content: '如有疑问,请查看帮助文档或联系技术支持。',
-
tips: [
-
'使用搜索功能快速找到感兴趣的内容',
-
'关注其他用户获取更新通知',
-
'完善个人资料让其他用户更好地了解您'
-
],
-
help_url: '/help'
-
}
-
end
-
end
-
-
def generate_recommendations
-
recommendations = []
-
-
# 基于用户行为推荐
-
recommendations << generate_activity_recommendations
-
recommendations << generate_content_recommendations
-
recommendations << generate_connection_recommendations
-
recommendations << generate_feature_recommendations
-
-
recommendations.flatten.select(&:itself).first(5)
-
end
-
-
def generate_activity_recommendations
-
# 基于用户参与的读书活动类型推荐相似活动
-
user_activities = user&.reading_events&.where('enrollments.created_at > ?', 30.days.ago)
-
-
return [] unless user_activities&.any?
-
-
similar_activities = ReadingEvent.where
-
.not(id: user_activities.pluck(:id))
-
.where(status: :active)
-
.where(category: user_activities.pluck(:category).uniq)
-
.limit(3)
-
-
similar_activities.map do |activity|
-
{
-
type: 'activity',
-
title: "推荐活动: #{activity.title}",
-
description: "基于您参与过的#{activity.category}类活动推荐",
-
action_url: "/events/#{activity.id}",
-
priority: 'high'
-
}
-
end
-
end
-
-
def generate_content_recommendations
-
# 基于用户点赞和评论推荐帖子
-
liked_categories = user&.likes&.joins(:post)
-
.where('likes.created_at > ?', 30.days.ago)
-
.group('posts.category')
-
.count
-
.keys
-
-
return [] unless liked_categories&.any?
-
-
popular_posts = Post.where
-
.category: liked_categories
-
.where('posts.created_at > ?', 7.days.ago)
-
.order(likes_count: :desc)
-
.limit(3)
-
-
popular_posts.map do |post|
-
{
-
type: 'content',
-
title: "热门帖子: #{post.title}",
-
description: "您感兴趣的#{post.category}类别中的热门内容",
-
action_url: "/posts/#{post.id}",
-
priority: 'medium'
-
}
-
end
-
end
-
-
def generate_connection_recommendations
-
# 推荐可能认识的用户
-
mutual_connections = find_mutual_connections
-
-
mutual_connections.first(3).map do |potential_user|
-
{
-
type: 'connection',
-
title: "可能认识的用户: #{potential_user.nickname}",
-
description: "您有#{mutual_connections[potential_user]}个共同好友",
-
action_url: "/users/#{potential_user.id}",
-
priority: 'low'
-
}
-
end
-
end
-
-
def generate_feature_recommendations
-
features = []
-
-
# 新功能推荐
-
unless user&.preferences&.dig('new_features_shown')&.include?('reading_goals')
-
features << {
-
type: 'feature',
-
title: '设置阅读目标',
-
description: '为自己设定每月阅读目标,跟踪阅读进度',
-
action_url: '/profile/reading-goals',
-
priority: 'high',
-
badge: 'NEW'
-
}
-
end
-
-
# 功能使用提示
-
if user&.posts&.count == 0
-
features << {
-
type: 'feature',
-
title: '发布第一条帖子',
-
description: '开始分享您的想法,与其他书友交流',
-
action_url: '/posts/new',
-
priority: 'medium'
-
}
-
end
-
-
features
-
end
-
-
def find_mutual_connections
-
# 简化版的共同好友推荐逻辑
-
# 实际实现可以基于更复杂的社交网络分析
-
User.joins(:received_flowers)
-
.where(flowers: { giver_id: user.friends.pluck(:id) })
-
.where.not(id: user.id)
-
.distinct
-
.limit(10)
-
end
-
-
def generate_personalization_data
-
{
-
user_level: calculate_user_level,
-
achievement_progress: get_achievement_progress,
-
reading_stats: get_reading_statistics,
-
engagement_metrics: get_engagement_metrics,
-
personalized_greeting: generate_personalized_greeting
-
}
-
end
-
-
def calculate_user_level
-
# 基于用户活跃度计算等级
-
score = 0
-
-
# 帖子贡献
-
score += (user&.posts&.count || 0) * 10
-
# 评论贡献
-
score += (user&.comments&.count || 0) * 5
-
# 点赞互动
-
score += (user&.likes&.count || 0) * 2
-
# 活动参与
-
score += (user&.event_enrollments&.count || 0) * 15
-
# 小红花获得
-
score += (user&.received_flowers&.count || 0) * 8
-
-
case score
-
when 0..50
-
{ level: 1, title: '新手书友', next_level_score: 51, current_score: score }
-
when 51..200
-
{ level: 2, title: '活跃书友', next_level_score: 201, current_score: score }
-
when 201..500
-
{ level: 3, title: '资深书友', next_level_score: 501, current_score: score }
-
when 501..1000
-
{ level: 4, title: '领读者', next_level_score: 1001, current_score: score }
-
else
-
{ level: 5, title: '读书达人', next_level_score: nil, current_score: score }
-
end
-
end
-
-
def get_achievement_progress
-
# 获取用户成就进度
-
achievements = [
-
{
-
id: 'first_post',
-
name: '初次分享',
-
description: '发布第一条帖子',
-
progress: (user&.posts&.count || 0) >= 1 ? 100 : 0,
-
completed: (user&.posts&.count || 0) >= 1,
-
icon: 'post'
-
},
-
{
-
id: 'ten_posts',
-
name: '积极分享',
-
description: '发布10条帖子',
-
progress: [(user&.posts&.count || 0) * 10, 100].min,
-
completed: (user&.posts&.count || 0) >= 10,
-
icon: 'posts'
-
},
-
{
-
id: 'first_comment',
-
name: '初次评论',
-
description: '发表第一条评论',
-
progress: (user&.comments&.count || 0) >= 1 ? 100 : 0,
-
completed: (user&.comments&.count || 0) >= 1,
-
icon: 'comment'
-
},
-
{
-
id: 'first_event',
-
name: '初次参与',
-
description: '参加第一个读书活动',
-
progress: (user&.event_enrollments&.count || 0) >= 1 ? 100 : 0,
-
completed: (user&.event_enrollments&.count || 0) >= 1,
-
icon: 'event'
-
}
-
]
-
-
# 添加自定义成就
-
achievements.concat(get_custom_achievements)
-
end
-
-
def get_custom_achievements
-
# 基于用户行为的自定义成就
-
custom_achievements = []
-
-
# 连续签到成就
-
check_in_streak = calculate_check_in_streak
-
if check_in_streak > 0
-
custom_achievements << {
-
id: 'check_in_streak',
-
name: "连续签到#{check_in_streak}天",
-
description: '坚持每日签到',
-
progress: [check_in_streak * 10, 100].min,
-
completed: check_in_streak >= 10,
-
icon: 'calendar'
-
}
-
end
-
-
# 社交达人成就
-
flowers_given = user&.flowers_given&.count || 0
-
if flowers_given > 0
-
custom_achievements << {
-
id: 'social_butterfly',
-
name: "送出#{flowers_given}朵小红花",
-
description: '积极互动,鼓励他人',
-
progress: [flowers_given * 2, 100].min,
-
completed: flowers_given >= 50,
-
icon: 'flower'
-
}
-
end
-
-
custom_achievements
-
end
-
-
def calculate_check_in_streak
-
# 计算连续签到天数
-
return 0 unless user
-
-
# 获取最近30天的签到记录
-
check_ins = CheckIn.where(user: user)
-
.where('created_at > ?', 30.days.ago)
-
.order(created_at: :desc)
-
-
return 0 if check_ins.empty?
-
-
streak = 1
-
check_ins.each_cons(2) do |current, previous|
-
break unless (current.created_at.to_date - previous.created_at.to_date) == 1
-
streak += 1
-
end
-
-
streak
-
end
-
-
def get_reading_statistics
-
{
-
books_read: user&.check_ins&.distinct.count(:reading_schedule_id) || 0,
-
pages_read: calculate_pages_read,
-
reading_time: calculate_reading_time,
-
favorite_genres: get_favorite_genres,
-
monthly_progress: get_monthly_progress
-
}
-
end
-
-
def calculate_pages_read
-
# 基于打卡数据估算阅读页数
-
user&.check_ins&.sum(:pages_read) || 0
-
end
-
-
def calculate_reading_time
-
# 基于打卡数据估算阅读时间
-
total_minutes = user&.check_ins&.sum(:reading_duration) || 0
-
hours = total_minutes / 60
-
minutes = total_minutes % 60
-
-
{ hours: hours, minutes: minutes, total_minutes: total_minutes }
-
end
-
-
def get_favorite_genres
-
# 获取用户最喜欢的阅读类型
-
genre_counts = CheckIn.joins(reading_schedule: :reading_event)
-
.where(user: user)
-
.group('reading_events.category')
-
.count
-
-
genre_counts.sort_by { |_, count| -count }.first(3).map do |genre, count|
-
{ genre: genre, count: count }
-
end
-
end
-
-
def get_monthly_progress
-
# 获取本月阅读进度
-
start_of_month = Time.current.beginning_of_month
-
-
{
-
posts_this_month: user&.posts&.where('created_at > ?', start_of_month).count || 0,
-
comments_this_month: user&.comments&.where('created_at > ?', start_of_month).count || 0,
-
events_this_month: user&.event_enrollments&.where('created_at > ?', start_of_month).count || 0,
-
flowers_this_month: user&.received_flowers&.where('created_at > ?', start_of_month).count || 0
-
}
-
end
-
-
def get_engagement_metrics
-
{
-
login_frequency: calculate_login_frequency,
-
interaction_rate: calculate_interaction_rate,
-
content_quality_score: calculate_content_quality_score,
-
community_contribution: calculate_community_contribution
-
}
-
end
-
-
def calculate_login_frequency
-
# 计算登录频率(简化版)
-
recent_logins = 30 # 假设数据,实际应从日志获取
-
(recent_logins / 30.0).round(2)
-
end
-
-
def calculate_interaction_rate
-
# 计算互动率
-
total_interactions = (user&.comments&.count || 0) + (user&.likes&.count || 0)
-
total_content = user&.posts&.count || 1
-
-
(total_interactions.to_f / total_content).round(2)
-
end
-
-
def calculate_content_quality_score
-
# 计算内容质量分
-
total_likes = user&.posts&.sum(:likes_count) || 0
-
total_comments = user&.posts&.sum(:comments_count) || 0
-
total_posts = user&.posts&.count || 1
-
-
score = ((total_likes * 2 + total_comments) / total_posts.to_f).round(2)
-
[score, 10.0].min
-
end
-
-
def calculate_community_contribution
-
# 计算社区贡献度
-
contribution_score = 0
-
-
# 发帖贡献
-
contribution_score += (user&.posts&.count || 0) * 5
-
# 评论贡献
-
contribution_score += (user&.comments&.count || 0) * 3
-
# 小红花贡献
-
contribution_score += (user&.flowers_given&.count || 0) * 2
-
# 活动贡献
-
contribution_score += (user&.event_enrollments&.count || 0) * 8
-
-
contribution_score
-
end
-
-
def generate_personalized_greeting
-
hour = Time.current.hour
-
time_greeting = case hour
-
when 5..11
-
'早上好'
-
when 12..17
-
'下午好'
-
when 18..22
-
'晚上好'
-
else
-
'夜深了'
-
end
-
-
user_name = user&.nickname || '书友'
-
activity_tip = generate_activity_tip
-
-
"#{time_greeting},#{user_name}!#{activity_tip}"
-
end
-
-
def generate_activity_tip
-
case Time.current.hour
-
when 5..9
-
'新的一天开始了,要不要读几页书?'
-
when 12..13
-
'午休时间,看看书友们的分享吧'
-
when 18..20
-
'晚饭后是阅读的好时光'
-
when 21..22
-
'睡前阅读,有助于睡眠'
-
else
-
'注意休息,别太晚了哦'
-
end
-
end
-
-
def generate_quick_actions
-
actions = []
-
-
case request_context[:current_page]
-
when 'home'
-
actions << { name: '发布新帖', url: '/posts/new', icon: 'edit' }
-
actions << { name: '查看活动', url: '/events', icon: 'calendar' }
-
when 'profile'
-
actions << { name: '编辑资料', url: '/profile/edit', icon: 'user' }
-
actions << { name: '设置', url: '/settings', icon: 'settings' }
-
end
-
-
# 基于用户状态添加快捷操作
-
if user&.unread_notifications&.any?
-
actions << { name: '查看通知', url: '/notifications', icon: 'bell', badge: user.unread_notifications.count }
-
end
-
-
actions
-
end
-
-
def generate_contextual_tips
-
tips = []
-
-
# 基于时间和用户行为的提示
-
if Time.current.hour >= 22
-
tips << {
-
type: 'health',
-
message: '夜深了,注意保护眼睛,适当休息',
-
icon: 'moon'
-
}
-
end
-
-
# 基于用户活跃度的提示
-
if user&.last_sign_in_at && user.last_sign_in_at < 7.days.ago
-
tips << {
-
type: 'engagement',
-
message: '您已经几天没有来了,看看朋友们的新动态吧',
-
icon: 'users'
-
}
-
end
-
-
# 新功能提示
-
if user&.created_at && user.created_at < 30.days.ago && user.posts.count < 3
-
tips << {
-
type: 'encouragement',
-
message: '分享您的读书心得,帮助更多书友',
-
icon: 'book'
-
}
-
end
-
-
tips
-
end
-
end